Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Double" composition with CompletableFuture

I'm trying to avoid nesting CompletableFuture when combining 2 independent ones with a BiFunction that returns a third one. Currently, using thenCombine() does not cut it:

// What I have
public CompletableFuture<CompletableFuture<C>> doStuff() {
    CompletableFuture<A> aFuture = makeSomeA();
    CompletableFuture<B> bFuture = makeSomeB();
    CompletableFuture<CompletableFuture<C>> cFuture = aFuture.thenCombine(bFuture, this::makeSomeC);
    return cFuture;
}

// What I want
public CompletableFuture<C> doStuff() {
    CompletableFuture<A> aFuture = makeSomeA();
    CompletableFuture<B> bFuture = makeSomeB();
    // obv this method does not exist
    CompletableFuture<C> c = aFuture.thenBicompose(bFuture, this::makeSomeC);
} 

private CompletableFuture<A> makeSomeA() {...}
private CompletableFuture<B> makeSomeB() {...}
private CompletableFuture<C> makeSomeC(A a, B b) {...}

I'm basically trying to find a way that looks like haskell if there was a CompletableFuture monad:

doStuff :: CompletableFuture C
doStuff = do
    a <- makeSomeA
    b <- makeSomeB
    makeSomeC a b

makeSomeA :: CompletableFuture A
makeSomeB :: CompletableFuture B
makeSomeC :: A -> B -> CompletableFuture C

I read somewhere that join() is the flatMap of Completable future, so I think I could use this method to do something like aFuture.thenCombine(bFuture, ((Function<CompletableFuture<C>,C>) CompletableFuture::join).compose(this::makeSomeC) but I'm not sure this is the proper/advised way to go. And I cannot say that this help readability in any way...

like image 221
Jerome Avatar asked Feb 13 '17 13:02

Jerome


2 Answers

To me it looks like thenCompose is the direct equivalent of the Haskell Monad.bind.

thenCompose can be nested in the same way as Haskell monad bind, which is also the result of a do-expression in Haskell. Using that your problem can be solved like this:

public CompletableFuture<C> doStuff() {
    CompletableFuture<A> aFuture = makeSomeA();
    CompletableFuture<B> bFuture = makeSomeB();
    return aFuture.thenCompose(a -> bFuture.thenCompose(b -> makeSomeC(a, b)));
}

Explanation of the types

This can be seen by inspecting the types of the functions.

Monad bind -- which is written >>= in Haskell -- has the following type:

(>>=) :: Monad m => m a -> (a -> m b) -> m b

thenCompose in Java has the following signature:

public <U> CompletionStage<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)

The above converted to Haskell syntax, with an extra parameter as an explicit this, looks like this:

thenCompose :: CompletionStage T -> (T -> CompletionStage U) -> CompletionStage U  

We can see that this has the same structure as the Haskell type. The difference is the names, and the fact Haskell's support for higher-kinded is not exactly expressed by Java interfaces.


Note on the Haskell code in the question

But I am a bit puzzled by your Haskell code. To me it looks like your Haskell code is doing the following:

public CompletableFuture<C> doStuff() {
    return makeSomeA().thenCompose(a -> makeSomeB().thenCompose(b -> makeSomeC(a, b)));
}

That is, waiting until the makeSomeA operation has completed before starting on makeSomeB. The Java code on the other hand starts the two operations in parallel, then waits for the result before starting on C. But maybe it is a laziness thing.

like image 198
Lii Avatar answered Sep 20 '22 07:09

Lii


I guess the simplest solution is just to apply a thenCompose(identity()) afterwards:

public CompletableFuture<C> doStuff() {
    CompletableFuture<A> aFuture = makeSomeA();
    CompletableFuture<B> bFuture = makeSomeB();
    CompletableFuture<CompletableFuture<C>> cFuture = aFuture.thenCombine(bFuture, this::makeSomeC);
    return cFuture.thenCompose(Function.identity());
}

Alternatively, introduce a simple Pair class to combine the results of A and B and use thenCompose():

public CompletableFuture<C> doStuff() {
    CompletableFuture<A> aFuture = makeSomeA();
    CompletableFuture<B> bFuture = makeSomeB();
    CompletableFuture<Pair<A, B>> cFuture = aFuture.thenCombine(bFuture, Pair::new);
    return cFuture.thenCompose(p -> makeSomeC(p.a, p.b));
}

private static class Pair<A, B> {
    A a;
    B b;

    public Pair(A a, B b) {
        this.a = a;
        this.b = b;
    }
}

And a third alternative without Pair:

public CompletableFuture<C> doStuff() {
    CompletableFuture<A> aFuture = makeSomeA();
    CompletableFuture<B> bFuture = makeSomeB();
    CompletableFuture<Void> cFuture = CompletableFuture.allOf(aFuture, bFuture);
    return cFuture.thenCompose(__ -> makeSomeC(aFuture.join(), bFuture.join()));
}
like image 30
Didier L Avatar answered Sep 18 '22 07:09

Didier L