Returning futures

When working with futures, one of the first things you’re likely to need to do is to return a Future. As with Iterators, however, doing so can be a little tricky. There are several options, listed from most to least ergonomic:

Trait objects

First, you always have the option of returning a boxed trait object:

# extern crate futures;
# use std::io;
# use futures::Future;
# fn main() {}
fn foo() -> Box<Future<Item = u32, Error = io::Error> + Send> {
    // ...
# loop {}
}

The upside of this strategy is that it’s easy to write down (just a Box) and easy to create. This is also maximally flexible in terms of future changes to the method as any type of future can be returned as an opaque, boxed Future.

The downside of this approach is that it requires a runtime allocation when the future is constructed, and dynamic dispatch when using that future. The Box needs to be allocated on the heap and the future itself is then placed inside. Note, though that this is the only allocation here, otherwise while the future is being executed no allocations will be made.

It’s often possible to mitigate that cost by boxing only at the end of a long chain of futures you want to return, which entails only a single allocation and dynamic dispatch for the entire chain.

Astute readers may notice the explicit Send trait notation within the Box definition. The notation is added because Future is not explicitly Send by default; this causes problems later when trying to pass this future or one of its derivatives into tokio::run.

impl Trait

If you are using a version of Rust greater than 1.26, then you can use the language feature impl Trait. This language feature will allow, for example:

fn add_10<F>(f: F) -> impl Future<Item = i32, Error = F::Error>
    where F: Future<Item = i32>,
{
    f.map(|i| i + 10)
}

Here we’re indicating that the return type is “something that implements Future” with the given associated types. Other than that we just use the future combinators as we normally would.

The upsides to this approach are that it is zero overhead with no Box necessary, it’s maximally flexible to future implementations as the actual return type is hidden, and it’s ergonomic to write as it’s similar to the nice Box example above. (You can even remove Send, because the compiler has enough information to determine at compile time that Future is Send!)

The downside to this approach is only that using a Box is still more flexible. If you might return two different types of Future, then you must still return Box<Future<Item = F::Item, Error = F::Error> + Send> instead of impl Future<Item = F::Item, Error = F::Error>. For the same reason, you cannot use impl Trait when defining or implementing your own traits – the compiler will not let you specify impl Trait as a return type in a trait implementation because it can’t determine which explicit type the trait should return. The good news however is that these cases are rare; in general, it should be a backwards-compatible extension to change return types from Box to impl Trait.

Named types

If you wouldn’t like to return a Box and want to stick with older versions of Rust, another option is to write the return type directly:

# extern crate futures;
# use futures::Future;
# use futures::future::Map;
# fn main() {}
fn add_10<F>(f: F) -> Map<F, fn(i32) -> i32>
    where F: Future<Item = i32>,
{
    fn do_map(i: i32) -> i32 { i + 10 }
    f.map(do_map)
}

Here we name the return type exactly as the compiler sees it. The map function returns the Map struct which internally contains the future and the function to perform the map.

The upside to this approach is that it doesn’t have the runtime overhead of Box from before, and works on Rust versions pre-1.26.

The downside, however, is that it’s often quite difficult to name the type. Sometimes the types can get quite large or be unnameable altogether. Here we’re using a function pointer (fn(i32) -> i32), but we would ideally use a closure. Unfortunately, the return type cannot name the closure, for now. It also leads to very verbose signatures, and leaks implementation details to clients.

Custom types

Finally, you can wrap the concrete return type in a new type, and implement future for it. For example:

struct MyFuture {
    inner: Sender<i32>,
}

fn foo() -> MyFuture {
    let (tx, rx) = oneshot::channel();
    // ...
    MyFuture { inner: tx }
}

impl Future for MyFuture {
    // ...
}

In this example we’re returning a custom type, MyFuture, and we implement the Future trait directly for it. This implementation leverages an underlying Oneshot<i32>, but any other kind of protocol can also be implemented here as well.

The upside to this approach is that it won’t require a Box allocation and it’s still maximally flexible. The implementation details of MyFuture are hidden to the outside world so it can change without breaking others.

The downside to this approach, however, is that this is the least ergonomic way to return futures.