Is there a more ergonomic syntax for Either when using futures?
Asked Answered
L

2

1

Here's an example of using Tokio to run a function that returns a future:

use futures::sync::oneshot;
use futures::Future;
use std::thread;
use std::time::Duration;
use tokio;

#[derive(Debug)]
struct MyError {
    error_code: i32,
}

impl From<oneshot::Canceled> for MyError {
    fn from(_: oneshot::Canceled) -> MyError {
        MyError { error_code: 1 }
    }
}

fn deferred_task() -> impl Future<Item = i32, Error = MyError> {
    let (sx, rx) = oneshot::channel();
    thread::spawn(move || {
        thread::sleep(Duration::from_millis(100));
        sx.send(100).unwrap();
    });
    return rx.map_err(|e| MyError::from(e));
}

fn main() {
    tokio::run(deferred_task().then(|r| {
        println!("{:?}", r);
        Ok(())
    }));
}

However, when the function in question (i.e. deferred_task) is non-trivial, the code becomes much more complex when I write it, because the ? operation doesn't seem to easily mix with returning a future:

fn send_promise_to_worker(sx: oneshot::Sender<i32>) -> Result<(), ()> {
    // Send the oneshot somewhere in a way that might fail, eg. over a channel
    thread::spawn(move || {
        thread::sleep(Duration::from_millis(100));
        sx.send(100).unwrap();
    });
    Ok(())
}

fn deferred_task() -> impl Future<Item = i32, Error = MyError> {
    let (sx, rx) = oneshot::channel();
    send_promise_to_worker(sx)?; // <-------- Can't do this, because the return is not a result
    return rx.map_err(|e| MyError::from(e));
}

A Future is a Result, it's meaningless to wrap it in result, and it breaks the impl Future return type.

Instead you get a deeply nested chain of:

fn deferred_task() -> impl Future<Item = i32, Error = MyError> {
    let (sx, rx) = oneshot::channel();
    match query_data() {
        Ok(_i) => match send_promise_to_worker(sx) {
            Ok(_) => Either::A(rx.map_err(|e| MyError::from(e))),
            Err(_e) => Either::B(futures::failed(MyError { error_code: 2 })),
        },
        Err(_) => Either::B(futures::failed(MyError { error_code: 2 })),
    }
}

full code

The more results you have, the deeper the nesting; exactly what the ? operator solves normally.

Am I missing something? Is there some syntax sugar to make this easier?

Leandra answered 14/3, 2019 at 5:26 Comment(0)
S
1

I do not see how async / await syntax will categorically help you with Either. Ultimately, you still need to return a single concrete type, and that's what Either provides. async / await will reduce the need for combinators like Future::map or Future::and_then however.

See also:


That being said, you don't need to use Either here.

You have consecutive Result-returning functions, so you can borrow a trick from JavaScript and use an IIFE to use use the ? operator. Then, we can "lift up" the combined Result into a future and chain it with the future from the receiver:

fn deferred_task() -> impl Future<Item = i32, Error = MyError> {
    let (tx, rx) = oneshot::channel();

    let x = (|| {
        let _i = query_data().map_err(|_| MyError { error_code: 1 })?;
        send_promise_to_worker(tx).map_err(|_| MyError { error_code: 2 })?;
        Ok(())
    })();

    future::result(x).and_then(|()| rx.map_err(MyError::from))
}

In the future, that IIFE could be replaced with a try block, as I understand it.

You could also go the other way and convert everything to a future:

fn deferred_task() -> impl Future<Item = i32, Error = MyError> {
    let (tx, rx) = oneshot::channel();

    query_data()
        .map_err(|_| MyError { error_code: 1 })
        .into_future()
        .and_then(|_i| {
            send_promise_to_worker(tx)
                .map_err(|_| MyError { error_code: 2 })
                .into_future()
        })
        .and_then(|_| rx.map_err(MyError::from))
}

This would be helped with async / await syntax:

async fn deferred_task() -> Result<i32, MyError> {
    let (tx, rx) = oneshot::channel();

    query_data().map_err(|_| MyError { error_code: 1 })?;

    send_promise_to_worker(tx).map_err(|_| MyError { error_code: 2 })?;

    let v = await! { rx }?;

    Ok(v)
}

I have also seen improved syntax for constructing the Either by adding left and right methods to the Future trait:

foo.left();
// vs
Either::left(foo);

However, this doesn't appear in any of the current implementations.

A Future is a Result

No, it is not.

There are two relevant Futures to talk about:

Notably, Future::poll returns a type that can be in two states:

  • Complete
  • Not complete

In the futures crate, "success" and "failure" are tied to "complete", whereas in the standard library they are not. In the crate, Result implements IntoFuture, and in the standard library you can use future::ready. Both of these allow converting a Result into a future, but that doesn't mean that Result is a future, no more than saying that a Vec<u8> is an iterator, even though it can be converted into one.

It's possible that the ? operator (powered by the Try trait), will be enhanced to automatically convert from a Result to a specific type of Future, or that Result will even implement Future directly, but I have not heard of any such plans.

Saucedo answered 14/3, 2019 at 15:31 Comment(0)
A
0

Is there some syntax sugar to make this easier?

Yes, it's called async/await, but it's not quite ready for wide consumption. It is only supported on nightly, it uses a slightly different version of futures that Tokio only supports via an interop library that causes additional syntactic overhead, and documentation for the whole thing is still spotty.

Here are some relevant links:

What is the purpose of async/await in Rust?
https://jsdw.me/posts/rust-asyncawait-preview/
https://areweasyncyet.rs/

Artless answered 14/3, 2019 at 6:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.