Like most of the Rust community, I’m excited about the recent developments in the async I/O space with the futures and tokio libraries. The futures library aims to be a zero-cost abstraction without allocation fit for high-performance I/O scenarios. In addition, the programming model built around the futures library makes writing async code elegant (and without penalty!).

Reading through the tutorial, it becomes obvious how futures are built: a future is simply an implementation of the Future trait that resolves to a value. The poll method drives the asynchronous computation.

This simplicity and elegance allow futures to be composable, either via the combinators exposed from the Future trait or through a custom struct or enum that also implements the Future trait. The chain of futures that compose a future encodes a state machine that builds the computation.

Beyond the Hello World

The combinators inside the futures library make composition easy. Chains of combinators make a great fit for the application layer where you’re implementing business logic.

However, when building a library or interface to other modules, you’ll likely want to create your own future. The easiest way to do so currently is to box the future (either via Box::new(...) or the provided .boxed() method from the Future trait), which allocates (at runtime) a trait object. This deviates from the stated goal of no allocation.

Given that creating futures is not a hard task, it makes sense to put in the extra effort to create an explicit type instead of relying on boxing inside of a library or common interface.

Here’s an example from one of my side projects:

This module exposes the LogFuture struct, without exposing its behavior to consumers of the module.

This is the same strategy the futures library itself uses to create combinators, because of the performance benefits.

Branching

Custom futures are great and easy to build, but I often find that I’m needing to branch within my code, even within a library. There could be a few different paths that lead to different future types.

Luckily, this can easily be achieved with enums, which describe the branching conditions.

This can get pretty repetitive and is certainly boilerplate code that can be eliminated.

I created a small library, union-future, that creates a combined future for you. The previous definition can be collapsed into a single macro invocation:

The enum ResFuture is created with 2 variants: Offset and Messages. The implementation of the Future trait has an Item type of Res and error type of std::io::Error . In order to map from the Offset and MessageSet types into the Res type, the From trait from std is derived and used by the future defined by the macro. This enables the code that uses the future to remain clean and ergonomic.

The library has one addition trick. The macro will derive the From trait on the each of the embedded Future types for you. This allows the function code that creates the future to use the . into() method, making the code extremely ergonomic yet efficient.

This code does not rely on boxing of futures, and it is as elegant to write as the boxed futures!