1

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?

Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145
  • 1
    How could `thenDo()` replace those methods, when they are different? In your naming, they would be `thenDoNow()` vs. `thenDoInBackground()`. They are not the same. – Andreas Jan 19 '18 at 23:04
  • "*Is it because it's dangerous or not possible to infer return types from lambdas in Java?*" - I wouldn't say that's the reason for distinguishing the two methods, but yes, that's a true statement. A lambda's type, including type parameters, is inferred from its context, not the other way around. – John Bollinger Jan 19 '18 at 23:08
  • @Andreas - I guess thought the compiler should already _know_ whether `thenDoNow()` or `thenDoBackground()` based on what is being returned in the lambda... – Andrew Cheong Jan 20 '18 at 02:55
  • 1
    @AndrewCheong On what's being *returned*? Don't you mean on what's being passed in? You're the one *telling* the method what *you* want it to do. – Andreas Jan 20 '18 at 05:49
  • @Andreas - It seems I've completely misunderstood the methods. Thanks. Back to the basics for me... Voting to self-close. – Andrew Cheong Jan 20 '18 at 06:09
  • The big problem is that the accepted answer of the linked question is not a good one. It reads like the purpose of `thenCompose` was what actually `thenApplyAsync` is for. The difference between `thenApply` and `thenCompose` is a fundamental one, deserving a different name. – Holger Jan 22 '18 at 12:19

2 Answers2

5

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

giorgiga
  • 1,758
  • 12
  • 29
2

Your understanding of the differences is wrong. Both thenApply and thenCompose return CompletableFuture's (or, well, CompletionStages).

The difference between them is what you've hidden in the (result) -> { ... } part.

For thenApply, you want that function to return a String to make the whole line return a CompleteableFuture<String>.

For thenCompose, you want that function to return a CompleteableFuture<String> to make the whole line return a CompleteableFuture<String>.

David Glasser
  • 1,438
  • 13
  • 21