10

The Java 8 coding style preferred by my colleagues is chaining asynchronous calls all the way through, e.g.

CompletionStage<E> someMethod() {
    return doSomething().thenCompose(a -> {
      // ...
      return b;
    }).thenCompose(b -> {
      // ...
      return c;
    }).thenCompose(c -> {
      // ...
      return d;
    }).thenApply(d -> {
      // ...
      return e;
    });
}

I have something like the above, but with an added challenge: I need to recall values retrieved within some of the lambdas, in later lambdas. For example,

CompletionStage<E> someMethod() {
    return doSomething().thenCompose(a -> {
      // ...
      Foo foo = fooDAO.getFoos(a);
      // ...
      return b;
    }).thenCompose(b -> {
      // ...
      return c;
    }).thenCompose(c -> {
      // ...
      Bar bar = barDAO.getBars(foo);
      // ...
      return d;
    }).thenApply(d -> {
      // ...
      return someResult(d, bar);
    });
}

When I declare Foo foo; and Bar bar; in the outside scope, I get errors about them not being final or effectively final. And I read about using a wrapper to make them effectively final, but it seems rather hacky to me to do this (I don't understand why that's allowed...)

I read that Java 8 didn't add support for tuples (though it considered BiVal, which maybe I could have used to pass to BiFunction lambdas). So I tried using pairs, e.g.

    return doSomething().thenCompose(a -> {
      // ...
      Foo foo = fooDAO.getFoos(a);
      // ...
      return new Pair<>(foo, b);
    }).thenCompose(fooAndB -> {

and then wherever I needed to recall foo,

      Foo foo = fooAndB.getKey();

But this feels so semantically wrong. Also, it doesn't work! I don't know why, because I thought a lambda parameter's scope is the same as its outside scope, and so all lambda parameters would be accessible from within later-chained lambdas.

What really is the scope of lambda parameters, and is there an idiomatic or at least semantically unoffensive way to do what I'd like to do while keeping the chaining?

Answers based on breaking the chain are fine as they may be useful for future viewers, but in my case, deviations from the dominant style in this repo may result in drawn out PR conversations and delayed approval, so I'd love a solution that preserves the chaining. Or, an explanation or demonstration as to how insane it would be to try to keep the chaining. Thank you!

Andrew Cheong
  • 29,362
  • 15
  • 90
  • 145
  • Try declare `AtomicReference ` in the outside scope. then use `get`method to get current value. – Hadi J Oct 05 '18 at 16:37
  • 1
    He mentioned he tried using wrappers (which is what using an AtomicReference here would be) and found it hacky. – RedDeckWins Oct 05 '18 at 16:40
  • Hm, I actually found a way that uses an array, but not `AtomicReference`. I guess I'm okay with it if I can just understand what difference the wrapper is making that makes the compiler happy all of a sudden... what was the purpose of the restriction without a wrapper? – Andrew Cheong Oct 05 '18 at 16:42
  • Ah, actually could you add your suggestion as an answer @HadiJ? Reading about `AtomicReference` helped me understand the concurrency issues that would occur if one did not properly wrap the variable in the outside scope. It was because the wrapper I found was simply an array with one element, that I found the strategy hacky. – Andrew Cheong Oct 05 '18 at 16:49
  • Using an AtomicReference would work as well, and depending on the concurrency of your methods, you could potentially create your own wrapper if you don't want or need the overhead of making it threadsafe. – RedDeckWins Oct 05 '18 at 16:54
  • 1
    The downside of using an AtomicReference is now your method is untestable except as a complete chain. If you pass the argument POJO down the chain you can potentially pull out the logic of each function into it's own method and unit test that. Then in your main, chaining method you provide method references. – RedDeckWins Oct 05 '18 at 17:00

3 Answers3

7

Since you mention the coding style preferred by your colleagues, you probably already know the alternative of using nested calls instead:

CompletionStage<E> someMethod() {
    return doSomething().thenCompose(a -> {
        // ...
        Foo foo = fooDAO.getFoos(a);
        // ...
        CompletableFuture<B> resultB = ...;
        return resultB.thenCompose(b -> {
            // ...
            CompletableFuture<C> resultC = ...;
            return resultC;
        }).thenCompose(c -> {
            // ...
            Bar bar = barDAO.getBars(foo);
            // ...
            CompletableFuture<D> resultD = ...;
            return resultD.thenApply(d -> {
                // ...
                return someResult(d, bar);
            });
        });
    });
}

This immediately fixes your issue, at the cost of a bit less readable code. But this problem can easily be fixed by extracting some methods from your code:

CompletionStage<E> someMethod() {
    return doSomething()
            .thenCompose(this::processA);
}

private CompletionStage<E> processA(final A a) {
    // ...
    Foo foo = fooDAO.getFoos(a);
    // ...
    final CompletableFuture<B> result = ...;
    return result
            .thenCompose(this::processB)
            .thenCompose(c -> processCAndFoo(c, foo));
}

private CompletionStage<C> processB(B b) {
    // ...
    return ...;
}

private CompletionStage<E> processCAndFoo(final C c, final Foo foo) {
    // ...
    Bar bar = barDAO.getBars(foo);
    // ...
    final CompletableFuture<D> result = ...;
    return result
            .thenApply(d -> someResult(d, bar));
}

By doing this, you avoid nested lambdas (and respect the preferred code style of your colleagues), but you also gain in readability and testability since you now have several small methods that are easier to understand and unit test.

Didier L
  • 18,905
  • 10
  • 61
  • 103
3

You can try this approach (I'm not necessarily an advocate of doing it this way but it does maintain the chain). You create a POJO that contains all the arguments you will need for the chain and pass the same POJO down the chain. The downside here is that it's kind of a pain to add additional methods to the chain, as you now have to also add a property to the argument class.

public class SomeMethodContext {

    private Object a;
    private Object b;
    private Object c;
    private Object d;
    private Object foo;
    private Object bar;

    // Getters and setters

}

// ...

CompletionStage<E> someMethod() {
    return doSomething().thenCompose(a -> {
      SomeMethodContext context = new SomeMethodContext();  
      context.setA(a);
      // ...
      context.setFoo(fooDAO.getFoos(context.getA()));
      // ...
      context.setB(b);
      return context;
    }).thenCompose(ctxt -> {
      // ...
      ctxt.setC(c);
      return ctxt;
    }).thenCompose(ctxt -> {
      // ...
      ctxt.setBar(barDAO.getBars(ctxt.getFoo()))
      // ...
      ctxt.setD(d)
      return ctxt;
    }).thenApply(ctxt -> {
      // ...
      return someResult(ctxt.getD(), ctxt.getBar());
    });
}
RedDeckWins
  • 2,111
  • 14
  • 16
  • I see, thanks. This answer hints to me that chaining is more of a style that must have emerged among users of the language, by taste, rather than something seriously worked on by the working group of the language... it seems to me chaining only works well insofar as tuples are not needed. – Andrew Cheong Oct 05 '18 at 16:45
  • I see you are warming up to the idea of using an AtomicReference to hold the value. That would work as well. – RedDeckWins Oct 05 '18 at 16:53
  • Using a wrapper this way is absolutely correct. Using a wrapper declared outside of the chain feels hacky (though it's possible). – fps Oct 05 '18 at 16:56
  • 1
    The problem is that with this kind of wrapper/context holder, you quickly loose the information about what is set, at which point and where. When the code grows, it is likely that this wrapper will get passed around several method and classes, making it very difficult to understand the dependencies. – Didier L Oct 05 '18 at 19:28
1

As another alternative, you might also consider introducing EA Async on your project. This API provides async/await for Java, and is particularly neat for this kind of problem. Here I rewrote your code with EA Async:

CompletionStage<E> someMethodWithEAAsync() {
    final A a = await(doSomething());
    Foo foo = fooDAO.getFoos(a);
    // ...
    CompletableFuture<B> futureB = ...;
    final B b = await(futureB);
    // ...
    CompletableFuture<C> futureC = ...;
    final C c = await(futureC);
    // ...
    Bar bar = barDAO.getBars(foo);
    // ...
    CompletableFuture<D> futureD = ...;
    D d = await(futureD);
    return completedFuture(someResult(d, bar));
    // or alternatively
    return futureD.thenApply(d -> someResult(d, bar));
}

It looks really similar to synchronous code, but behind the scenes EA Async will transform this method in order to keep everything asynchronous. No need to worry about chaining anymore!

Didier L
  • 18,905
  • 10
  • 61
  • 103