Search code examples
javalambdalanguage-designcompletable-future

What is a case where `thenApply()` vs. `thenCompose()` is ambiguous despite the return type of the lambda?


I'm learning about CompletableFutures.

I am not asking about the difference between thenApply() and thenCompose(). Instead, I want to ask about a code "scent" that doesn't feel right, and what might actually justify it.

From the usages of CompletableFutures I've seen so far, it seems you'd never have this:

CompletableFuture<String> foo = getSomething().thenApply((result) -> { ... });

Nor this:

String foo = getSomething().thenCompose((result) -> { ... });

To return a future, you have to use thenCompose(), and otherwise thenApply().

From experience though, it seems weird that language didn't devise a way to eliminate making this unambiguous choice every time. For example, couldn't there have been a single method thenDo() whose return type is inferred (during compile-time) from the return within the lambda? It could then be given thenApply or thenCompose-like properties at compile-time as well.

But I'm sure there's a good reason for having separate methods, so I'd like to know why.

  • Is it because it's dangerous or not possible to infer return types from lambdas in Java? (I'm new to Java as well.)

  • Is it because there is a case in which a single method would indeed be ambiguous, and the only solution is to have separate methods? (I'm imagining maybe, nested CompletableFutures or complicated interfaces and generics.) If so, can someone provide a clear example?

  • Is it for some other reason or documented recommendation?


Solution

  • For reference, the signatures of the two methods are:

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

    Function<? super T,? extends U> and Function<? super T,? extends CompletionStage<U>> have Function<? super T, ?> as a common supertype (ok, technically it's just Function).

    Therefore the signature of thenDo would be something like:

    <U> CompletableFuture<U> thenDo(Function<? super T,?> fn)
    

    which, while legal, would be a real pain to use as the compiler would have no way to check whether the return type for fn is correct and would have to accept anything.

    Also, the implementation of this thenDo would have no other option but to apply the function and check whether the returned object implements CompletionStage, which (besides being slow and... repugnantly inelegant) would have real issues in non-straightforward cases: what would happen when calling thenDo on a CompletableFuture<CompletionStage<String>>?


    If you are new to java generics, my recommendation is to concentrate on understanding two things first and foremost:

    1. Covariance/contravariance of type parameters (or rather lack thereof). Why isn't List<String> a subtype of List<Object>? What's the difference between List<Object> and List<?>?
    2. Type erasure. Why can't I overload a method based on a generic parameter?

    After you have those set, investigate how type variables can be resolved via reflection (eg: understand how Guava's TypeToken works)


    edit: fixed a link