How do I conditionally return different types of futures?
I have a method that, depending on a predicate, will return one future or another. In other words, an if-else expression that returns a future:
extern crate futures; // 0.1.23
use futures::{future, Future};
fn f() -> impl Future<Item = usize, Error = ()> {
if 1 > 0 {
future::ok(2).map(|x| x)
} else {
future::ok(10).and_then(|x| future::ok(x + 2))
}
}
This doesn't compile:
error[E0308]: if and else have incompatible types
--> src/lib.rs:6:5
|
6 | / if 1 > 0 {
7 | | future::ok(2).map(|x| x)
8 | | } else {
9 | | future::ok(10).and_then(|x| future::ok(x + 2))
10 | | }
| |_____^ expected struct `futures::Map`, found struct `futures::AndThen`
|
= note: expected type `futures::Map<futures::FutureResult<{integer}, _>, [closure@src/lib.rs:7:27: 7:32]>`
found type `futures::AndThen<futures::FutureResult<{integer}, _>, futures::FutureResult<{integer}, _>, [closure@src/lib.rs:9:33: 9:54]>`
The futures are created differently, and might hold closures, so their types are not equal. Ideally, the solution wouldn't use Box
es, since the rest of my async logic doesn't use them.
How is if-else logic in futures normally done?
Using async
/await
Since Rust 1.39, you can use async
and await
syntax to cover most cases:
async fn a() -> usize {
2
}
async fn b() -> usize {
10
}
async fn f() -> usize {
if 1 > 0 {
a().await
} else {
b().await + 2
}
}
See also:
- What is the purpose of async/await in Rust?
Either
Using futures::future::Either
via the FutureExt
trait has no additional heap allocation:
use futures::{Future, FutureExt}; // 0.3.5
async fn a() -> usize {
2
}
async fn b() -> usize {
10
}
fn f() -> impl Future<Output = usize> {
if 1 > 0 {
a().left_future()
} else {
b().right_future()
}
}
However, this requires a fixed stack allocation. If A
takes 1 byte and happens 99% of the time, but B
takes up 512 bytes, your Either
will always take up 512 bytes (plus some). This isn't always a win.
This solution also works for Stream
s.
Boxed trait objects
Here we use FutureExt::boxed
to return a trait object:
use futures::{Future, FutureExt}; // 0.3.5
async fn a() -> usize {
2
}
async fn b() -> usize {
10
}
fn f() -> impl Future<Output = usize> {
if 1 > 0 {
a().boxed()
} else {
b().boxed()
}
}
This solution also works for Stream
s.
As Matthieu M. points out, the two solutions can be combined:
I would note that there is a middle ground solution for the case of a large
B
:Either(A, Box<B>)
. This way, you only pay for the heap allocation on the rare case where it's aB
Note that you can also stack Either
s if you have more than 2 conditions (Either<A, Either<B, C>>
; Either<Either<A, B>, Either<C, D>>
, etc.):
use futures::{Future, FutureExt}; // 0.3.5
async fn a() -> i32 {
2
}
async fn b() -> i32 {
0
}
async fn c() -> i32 {
-2
}
fn f(v: i32) -> impl Future<Output = i32> {
use std::cmp::Ordering;
match v.cmp(&0) {
Ordering::Less => a().left_future(),
Ordering::Equal => b().left_future().right_future(),
Ordering::Greater => c().right_future().right_future(),
}
}
See also:
- Conditionally iterate over one of several possible iterators