Chaining operations

Sync or Async

To chain operations synchronously, you typically write statements one after the other, using the results of the previous statements to perform new statements:

// Build an animate action.
val animate: Animate = ...
// Run the animate action synchronously.
animate.run()
// Build a listen action.
val listen: Listen = ...
// Run the listen action synchronously.
listen.run()
// Build an animate action.
Animate animate = ...;
// Run the animate action synchronously.
animate.run();
// Build a listen action.
Listen listen = ...;
// Run the listen action synchronously.
listen.run();

Here, we run an Animate and then a Listen both synchronously.

These operations can also be executed asynchronously:

// Run the animate action asynchronously.
animate.async().run()
// Run the animate action asynchronously.
animate.async().run();
// Run the listen action asynchronously.
listen.async().run()
// Run the listen action asynchronously.
listen.async().run();

These pieces of code execute the asynchronous operations, but they are not linked together: both actions will start at the same time.

What we want here is to execute the second operation when the first one provides its result (here listen when the animation finishes). We will see below how to chain these operations asynchronously.

Futures

Note

In the following sections, the mentioned Future class corresponds to the com.aldebaran.qi.Future class. When using the Future class in your code, make sure to import com.aldebaran.qi.Future, and not java.util.concurrent.Future.

What is a future

A Future is an object that wraps an operation asynchronously.

It allows you to perform asynchronous operations while writing code in a sequential way.

The Future class is mainly used to perform the following operations asynchronously:

  • create objects like actions and resources,
  • handle an action execution (result, cancellation and error),
  • chain several action executions / resource creations.

State of a future

A Future has 3 states: it can provide a result, it can be cancelled or it can fail. These states correspond to the different end states of the wrapped operation.

If the wrapped operation:

  • returns a value, the Future will provide this value when available,
  • is cancelled, the Future will be cancelled,
  • encounters an error, the Future will fail.

Future<T> is a generic class, where T is the type of the value it can provide. For example, a Future<String> instance can provide a String value.

Note

If the wrapped operation does not return anything, the type of the future is Future<Void>. In that case, if the operation is successful, the Future<Void> will end in a result state.

The Future class provides the get method that gives a synchronous access to the value of the Future:

val stringFuture: Future<String> = ...
val value: String = stringFuture.get() // Synchronous call to get the value.
Future<String> stringFuture = ...;
String value = stringFuture.get(); // Synchronous call to get the value.

Calling the get method blocks the current thread: this call is synchronous.

Reminder

A synchronous call is blocking whereas an asynchronous one returns immediately.

If an error occurs in the wrapped operation, the get call will throw an ExecutionException and if the operation is cancelled, the get call will throw a CancellationException.

Use a try-catch to determine in which state the Future finishes:

val stringFuture: Future<String> = ...
try {
    val value: String = stringFuture.get()
    // The future finished with value.
} catch (e: ExecutionException) {
    // The future finished with error.
} catch (e: CancellationException) {
    // The future was cancelled.
}
Future<String> stringFuture = ...;
try {
    String value = stringFuture.get();
    // The future finished with value.
} catch (ExecutionException e) {
    // The future finished with error.
} catch (CancellationException e) {
    // The future was cancelled.
}

Operators

The Future class contains several methods that allow you to chain several asynchronous operations together:

  • thenConsume,
  • thenApply,
  • thenCompose,
  • andThenConsume,
  • andThenApply,
  • andThenCompose.

then / andThen

With “then…”:

then... methods allow you to chain 2 operations, regardless of the first operation end state. They are useful when you want to chain independent operations.

chaining_diagram_then_generic

then... gives you access to the Future wrapping the first operation. Use the isSuccess, hasError and isCancelled methods to determine the state of the Future:

val future: Future<String> = ...
future.thenConsume { stringFuture ->
    if (stringFuture.isSuccess) {
        // Handle success state.
        // Access the value with stringFuture.get().
    } else if (stringFuture.isCancelled) {
        // Handle cancelled state.
    } else {
        // Handle error state.
        // Access the error with stringFuture.error.
    }
}
Future<String> future = ...;
future.thenConsume(stringFuture -> {
    if (stringFuture.isSuccess()) {
        // Handle success state.
        // Access the value with stringFuture.get().
    } else if (stringFuture.isCancelled()) {
        // Handle cancelled state.
    } else {
        // Handle error state.
        // Access the error with stringFuture.getError().
    }
});

With “andThen…”:

andThen... methods allow you to chain 2 operations if and only if the first one ends with a result state. They are useful when some operation depends on the previous one in your chain.

chaining_diagram_andThen_generic

andThen... directly gives you access to the value of the Future wrapping the first operation:

val future: Future<String>  = ...
future.andThenConsume { string ->
    // Here you have access to the String.
}
Future<String> future = ...;
future.andThenConsume(string -> {
    // Here you have access to the String.
});

Tip

Think of andThen... as the boolean & operator: the right side is not evaluated if the left side is false.

Returned future:

The returned Future wraps the operation equivalent to the sequence of the 2 operations.

“then…” example:

We want Pepper to execute a dance made of several animations. The animations must be chained together and we continue the dance even if one animation fails: we use then....

chaining_diagram_then_example

// Build an animate action.
val firstAnimate: Animate = ...
// Run the animate action asynchronously.
val firstDance: Future<Void> = firstAnimate.async().run()

// Chain several animations.
val ballet: Future<Void>  = firstDance.thenCompose { dance ->
    // Build an animate action.
    val secondAnimate: Animate = ...
    // Run the animate action asynchronously.
    val secondDance: Future<Void> = secondAnimate.async().run()
    return secondDance
}
// Build an animate action.
Animate firstAnimate = ...;
// Run the animate action asynchronously.
Future<Void> firstDance = firstAnimate.async().run();

// Chain several animations.
Future<Void> ballet = firstDance.thenCompose(dance -> {
    // Build an animate action.
    Animate secondAnimate = ...;
    // Run the animate action asynchronously.
    Future<Void> secondDance = secondAnimate.async().run();
    return secondDance;
});

“andThen…” example:

We want Pepper to mimic an animal and then make someone guess this animal. Pepper should listen to the answer if and only if the animation succeeds: we use andThen....

chaining_diagram_andThen_example

// Build an animate action.
val animate: Animate = ...
// Run the animate action asynchronously.
val imitation: Future<Void> = animate.async().run()

// Chain the animation with a listen action.
val guess: Future<ListenResult> = imitation.andThenCompose { animateFuture ->
    // Build a listen action.
    val listen: Listen = ...
    // Run the listen action asynchronously.
    val answerListening: Future<ListenResult> = listen.async().run()
    return answerListening
}
// Build an animate action.
Animate animate = ...;
// Run the animate action asynchronously.
Future<Void> imitation = animate.async().run();

// Chain the animation with a listen action.
Future<ListenResult> guess = imitation.andThenCompose(animateFuture -> {
    // Build a listen action.
    Listen listen = ...;
    // Run the listen action asynchronously.
    Future<ListenResult> answerListening = listen.async().run();
    return answerListening;
});

consume / apply / compose

Depending on what your second operation returns, you will use one of the following methods:

  • ...Consume,
  • ...Apply,
  • ...Compose.

consume:

Use ...Consume to consume the Future and return nothing:

val future: Future<String>  = ...
val returnedOperation: Future<Void> = future.andThenConsume { Log.i(TAG, "Success: $it") }
Future<String> future = ...;
Future<Void> returnedOperation = future.andThenConsume(string -> Log.i(TAG, "Success: " + string));

apply:

Use ...Apply to apply a transformation to the Future’s value and return a specific type:

val future: Future<String>  = ...
val returnedOperation: Future<Int> = future.andThenApply { string -> string.length }
Future<String> future = ...;
Future<Integer> returnedOperation = future.andThenApply(string -> string.length());

compose:

Use ...Compose to return a Future. This is mainly used to chain actions:

val say: Say = ...
val sayFuture: Future<Void> = say.async().run();
val returnedOperation: Future<Void> = sayFuture.andThenCompose {
    val animate: Animate = ...
    val animateFuture: Future<Void> = animate.async().run()
    return animateFuture
}
Say say = ...;
Future<Void> sayFuture = say.async().run();
Future<Void> returnedOperation = sayFuture.andThenCompose(ignore -> {
    Animate animate = ...;
    Future<Void> animateFuture = animate.async().run();
    return animateFuture;
});

Operation’s state:

The operation represented by the lambda has different states depending on what is done inside this lambda:

Operation State
  If lambda throws Else
Consume Error Success
Apply Error Success
Compose Error Inner Future’s state*

The inner Future is the one returned by the lambda when using ...Compose.

Callbacks

Each chaining operator takes a different callback as parameter:

Operator andThen… then…
…Consume Consumer<T> Consumer<Future<T>>
…Apply Function<T, R> Function<Future<T>, R>
…Compose Function<T, Future<R>> Function<Future<T>, Future<R>>

Consumer

The Consumer interface contains the consume method. It is used to consume the result of a Future.

Note

The consume method of the Consumer interface is executed on a background thread.

Example:

Log the Future’s result:

val future: Future<String>  = ...
future.andThenConsume { string -> Log.i(TAG, "Success: $string") }
// Java 8:
Future<String> future = ...;
future.andThenConsume(string -> Log.i(TAG, "Success: " + string));

// Java 7:
Future<String> future = ...;
future.andThenConsume(new Consumer<String>() {
    @Override
    public void consume(String string) throws Throwable {
        Log.i(TAG, "Success: " + string);
    }
});

Function

The Function interface contains the execute method. It is used to return a new value / Future.

Note

The execute method of the Function interface is executed on a background thread.

Examples:

Transform the Future’s result:

val future: Future<String> = ...
future.andThenApply { string -> string.length }
// Java 8:
Future<String> future = ...;
future.andThenApply(string -> string.length());

// Java 7:
Future<String> future = ...;
future.andThenApply(new Function<String, Integer>() {
    @Override
    public Integer execute(String string) throws Throwable {
        return string.length();
    }
});

Chain actions:

val animate: Animate = ...
animate.async().run().andThenCompose {
    val say: Say = ...
    return say.async().run()
}
// Java 8:
Animate animate = ...;
animate.async().run().andThenCompose(ignore -> {
    Say say = ...;
    return say.async().run();
});

// Java 7:
Animate animate = ...;
animate.async().run().andThenCompose(new Function<Void, Future<Void>>() {
    @Override
    public Future<Void> execute(Void ignore) throws Throwable {
        Say say = ...;
        return say.async().run();
    }
});

See also