Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 Boxes, since the rest of my async logic doesn't use them.

How is if-else logic in futures normally done?

like image 697
Dominykas Mostauskis Avatar asked Aug 16 '18 21:08

Dominykas Mostauskis


1 Answers

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 Streams.

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 Streams.


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 a B

Note that you can also stack Eithers 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
like image 138
Shepmaster Avatar answered Nov 01 '22 02:11

Shepmaster