Home > Net >  How can I pass data through a chain of lambdas where the parameter type is inferred based on the ret
How can I pass data through a chain of lambdas where the parameter type is inferred based on the ret

Time:09-22

I am trying to run several tasks (lambda functions) one after another that transform some data. Something like:

// Pass some initial data in the constructor or just generate some in the first lambda
// The execute is a callback with the final data because the task will be async
new MyTask(null).start((d) -> {
    return 1.25f;
}).then((f) -> {
    return (int)(f*4);
}).then((i) -> {
    return "" i;
}).then((s) -> {
    return "The answer is " s;
}).execute((r) -> {
    System.out.println(r);
});

I've been trying to wrap my head around the generics needed to pull off something like this but I don't think I have a clear enough understanding of generics yet to know if or how this is possible.

Basically I have a functional interface like this:

@FunctionalInterface
public interface Middleware<I, O> {
    O run(I data);
}

and then I have a MyTask class, something like this:

public class MyTask<T> {
    private List<Middleware<?, ?>> middlewares = new ArrayList<>();
    private T data;

    public MyTask(T data) {
        this.data = data;
    }

    public <O> MyTask<T> start(Middleware<T, O> middleware) {
        middlewares.add(middleware);
        return this;
    }
    public <I, O> MyTask<T> then(Middleware<I, O> middleware) {
        middlewares.add(middleware);
        return this;
    }
    public void execute(Callback<?> callback) {
        // not sure what to do here, how to run the middlewares and callback with
        // the last return value
    }
}

So I was storing the lambdas in a List<Middleware> whenever I call then(...), but I don't know how to actually run through the list and put the data in, then take the result and pass it to the next one. I think I need to use generics because I need the types to be inferred, but I'm a little confused and can't seem to get it working. Any help would be greatly appreciated. I don't even know if what I have so far is in the right direction.

CodePudding user response:

All you are basically doing here is functional composition.

For that, the standard thing to do is to use the Function interface, and the Function.andThen method (and/or Function.compose, which applies the new function before the current one).

Function<A, B> function1 = ...
Function<B, C> function2 = ...

Function<A, C> composed = function1.andThen(function2);

composed applies function1 to the input, then takes the result and applies function2.

Even if you want to attempt to implement a similar mechanism yourself, it is probably worth studying how Function achieves this to give you some ideas.


The essential point to achieve what I think you're trying to do is that you don't want your composition methods to be returning this: you only actually need MyTask to capture the output type, since the composition and callbacks don't need to care about the initial input's type.

public class MyTask<OutT> {
  private final Supplier<OutT> supplier;

  private MyTask(Supplier<OutT> supplier) {
    this.supplier = supplier;
  }

Notice that you don't actually need to keep the data and middleware separate: you always use data as the input to middleware, so you're effectively just supplying a value: all you need to store is a Supplier<OutT>.

Now, let's add some things to this. Note that I made the constructor private: this is because I want to add a factory method, so you can't just create an instance with any old types:

  public <InT> MyTask<InT> create(InT data) {
    return new MyTask<>(() -> data);
  }

This creates an identity instance, that converts the input data to itself. Kinda dull, but let's take it from there.

With an identity transformation, you don't need a start method any more, because you've already "started". But let's add a then:

  public <NewOutT> MyTask<NewOutT> then(Middleware<OutT, NewOutT> middleware) {
    return new MyTask<>(() -> middleware.run(supplier.get()));
}

So, you pass a middleware which can transform the value supplied by the supplier, and you compose them together.

And then you can have an execute method, taking a callback which takes an instance of OutT:

  public void execute(Callback<OutT> callback) {
    callback.accept(supplier.get());
  }
}

(Omitted for clarity above: you can also use wildcards to make the method parameters more flexible, e.g. Middleware<A, B> middleware -> Middleware<? super A, ? extends B> middleware and Callback<? super OutT> callback).

  • Related