Returning futures
When working with futures, one of the first things you’re likely to need to dois to return a Future
. As with Iterator
s, 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
) andeasy to create. This is also maximally flexible in terms of future changes tothe 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 thefuture is constructed, and dynamic dispatch when using that future. The Box
needs to be allocated on the heap and the future itself is then placedinside. Note, though that this is the only allocation here, otherwise whilethe 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 longchain of futures you want to return, which entails only a single allocation anddynamic 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
bydefault; this causes problems later when trying to pass this future or one of itsderivatives into tokio::run
.
impl Trait
If you are using a version of Rust greater than 1.26, then you can use thelanguage feature impl Trait
. This language feature will allow, forexample:
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 implementsFuture
” with the given associated types. Other than that we just use thefuture 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 actualreturn type is hidden, and it’s ergonomic to write as it’s similar to the niceBox
example above. (You can even remove Send
, because the compiler hasenough information to determine at compile time that Future
is Send
!)
The downside to this approach is only that using a Box
is still moreflexible. If you might return two different types of Future
, then youmust still return Box<Future<Item = F::Item, Error = F::Error> + Send>
insteadof impl Future<Item = F::Item, Error = F::Error>
. For the same reason, youcannot use impl Trait
when defining or implementing your own traits – thecompiler will not let you specify impl Trait
as a return type in a traitimplementation 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 abackwards-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 ofRust, 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 thefunction to perform the map.
The upside to this approach is that it doesn’t have the runtime overhead ofBox
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’reusing 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 tovery verbose signatures, and leaks implementation details to clients.
Custom types
Finally, you can wrap the concrete return type in a new type, and implementfuture 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 theFuture
trait directly for it. This implementation leverages an underlyingOneshot<i32>
, but any other kind of protocol can also be implemented here aswell.
The upside to this approach is that it won’t require a Box
allocation and it’sstill maximally flexible. The implementation details of MyFuture
are hidden tothe outside world so it can change without breaking others.
The downside to this approach, however, is that this is the least ergonomic wayto return futures.
Next up: Working with framed streams