Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to nicely do allOf/AnyOf with Collections of CompletionStage

Currently to do something simple with Collections of CompletionStage requires jumping through several ugly hoops:

public static CompletionStage<String> translate(String foo) {
    // just example code to reproduce
    return CompletableFuture.completedFuture("translated " + foo);
}

public static CompletionStage<List<String>> translateAllAsync(List<String> input) {
    List<CompletableFuture<String>> tFutures = input.stream()
        .map(s -> translate(s)
            .toCompletableFuture())
        .collect(Collectors.toList()); // cannot use toArray because of generics Arrays creation :-(
    return CompletableFuture.allOf(tFutures.toArray(new CompletableFuture<?>[0])) // not using size() on purpose, see comments
        .thenApply(nil -> tFutures.stream()
            .map(f -> f.join())
            .map(s -> s.toUpperCase())
            .collect(Collectors.toList()));
}

What I want to write is:

public CompletionStage<List<String>> translateAllAsync(List<String> input) {
    // allOf takes a collection< futures<X>>, 
    // and returns a future<collection<x>> for thenApply()
    return XXXUtil.allOf(input.stream() 
            .map(s -> translate(s))
            .collect(Collectors.toList()))
        .thenApply(translations -> translations.stream()
            .map(s -> s.toUpperCase())
            .collect(Collectors.toList()));
}

The whole ceremony about toCompletableFuture and converting to an Array and join is boilerplate distracting from the actual code semantics.

Possibly having a version of allOf() returning a Future<Collection<Future<X>>> instead of Future<Collection<X>> may also be useful in some cases.

I could try implementing XXXUtil myself, but I wonder if there already is a mature 3rdparty library for this and similar issues (Such as Spotify's CompletableFutures). If so, I'd like to see the equivalent code for such a library as an answer.

Or maybe the original code posted above can somehow be written more compactly in a different way?

JUnit test code:

@Test
public void testTranslate() throws Exception {
    List<String> list = translateAllAsync(Arrays.asList("foo", "bar")).toCompletableFuture().get();
    Collections.sort(list);
    assertEquals(list,
        Arrays.asList("TRANSLATED BAR", "TRANSLATED FOO"));
}
like image 289
tkruse Avatar asked Apr 20 '18 04:04

tkruse


1 Answers

I just looked into the source code of CompletableFuture.allOf, to find that it basically creates a binary tree of nodes handling two stages at a time. We can easily implement a similar logic without using toCompletableFuture() explicitly and handling the result list generation in one go:

public static <T> CompletionStage<List<T>> allOf(
                  Stream<? extends CompletionStage<? extends T>> source) {
    return allOf(source.collect(Collectors.toList()));
}
public static <T> CompletionStage<List<T>> allOf(
                  List<? extends CompletionStage<? extends T>> source) {
    int size = source.size();
    if(size == 0) return CompletableFuture.completedFuture(Collections.emptyList());
    List<T> result = new ArrayList<>(Collections.nCopies(size, null));
    return allOf(source, result, 0, size-1).thenApply(x -> result);
}
private static <T> CompletionStage<Void> allOf(
                   List<? extends CompletionStage<? extends T>> source,
                   List<T> result, int from, int to) {
    if(from < to) {
        int mid = (from+to)>>>1;
        return allOf(source, result, from, mid)
            .thenCombine(allOf(source, result, mid+1, to), (x,y)->x);
    }
    return source.get(from).thenAccept(t -> result.set(from, t));
}

That’s it.

You can use this solution to implement the logic of your question’s code as

public static CompletionStage<List<String>> translateAllAsync(List<String> input) {
    return allOf(input.stream().map(s -> translate(s)))
        .thenApply(list -> list.stream()
            .map(s -> s.toUpperCase())
            .collect(Collectors.toList()));
}

though it would be more natural to use

public static CompletionStage<List<String>> translateAllAsync(List<String> input) {
    return allOf(input.stream().map(s -> translate(s).thenApply(String::toUpperCase)));
}

Note that this solution maintains the order, so there is no need for sorting the result in the test case:

@Test
public void testTranslate() throws Exception {
    List<String> list = translateAllAsync(Arrays.asList("foo", "bar")).toCompletableFuture().get();
    assertEquals(list, Arrays.asList("TRANSLATED FOO", "TRANSLATED BAR"));
}
like image 147
Holger Avatar answered Sep 23 '22 23:09

Holger