DEV Community

Francesco Cogno
Francesco Cogno

Posted on • Edited on

Rust futures: an uneducated, short and hopefully not boring tutorial - Part 2

Intro

In the first post of the series we saw how to use futures in Rust. They are powerful and very easy to use, if done properly. In this second post we will focus on common pitfalls and, hopefully, how to avoid them.

Error troubles

Chaining futures is easy. We saw how to exploit the and_then() function to do it. But in the previous part we cheated using the Box<Error> trait as error type. Why haven't I used a more specific error type? It turns out when you chain futures you must return always the same error type.

While chaining futures the error type must be the same.

Let's try and demonstrate this.

We have two types, called ErrorA and ErrorB (yes, I know, not terribly original). We will implement the error::Error trait even though is not strictly necessary (but it's good practice IMHO). The Error trait requires std::fmt::Display too so we're going to implement that trait as well.

#[derive(Debug, Default)]
pub struct ErrorA {}

impl fmt::Display for ErrorA {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "ErrorA!")
    }
}

impl error::Error for ErrorA {
    fn description(&self) -> &str {
        "Description for ErrorA"
    }

    fn cause(&self) -> Option<&error::Error> {
        None
    }
}
Enter fullscreen mode Exit fullscreen mode

ErrorB is the same:

#[derive(Debug, Default)]
pub struct ErrorB {}

impl fmt::Display for ErrorB {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "ErrorB!")
    }
}

impl error::Error for ErrorB {
    fn description(&self) -> &str {
        "Description for ErrorB"
    }

    fn cause(&self) -> Option<&error::Error> {
        None
    }
}
Enter fullscreen mode Exit fullscreen mode

I've kept the implementations simple to prove my point. Now let's use these structs in out futures. Remember, a future it's just a Result<A,B> returning function with a slight different syntax.

fn fut_error_a() -> impl Future<Item = (), Error = ErrorA> {
    err(ErrorA {})
}

fn fut_error_b() -> impl Future<Item = (), Error = ErrorB> {
    err(ErrorB {})
}
Enter fullscreen mode Exit fullscreen mode

Now let's call them in our main function:

let retval = reactor.run(fut_error_a()).unwrap_err();
println!("fut_error_a == {:?}", retval);

let retval = reactor.run(fut_error_b()).unwrap_err();
println!("fut_error_b == {:?}", retval);
Enter fullscreen mode Exit fullscreen mode

The result is, unsurprisingly:

fut_error_a == ErrorA
fut_error_b == ErrorB
Enter fullscreen mode Exit fullscreen mode

So far so good. Now let's try to chain these futures:

let future = fut_error_a().and_then(|_| fut_error_b());
Enter fullscreen mode Exit fullscreen mode

What we are doing here is to call the fut_error_a function and then call fut_error_b one. We do not care about the value returned by fut_error_a so we discard it with an underscore.
In more complex terms we want to chain a impl Future<Item=(), Error=ErrorA> with a impl Future<Item=(), Error=ErrorB>.

Let's try and compile it:

Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2)
error[E0271]: type mismatch resolving `<impl futures::Future as futures::IntoFuture>::Error == errors::ErrorA`
   --> src/main.rs:166:32
    |
166 |     let future = fut_error_a().and_then(|_| fut_error_b());
    |                                ^^^^^^^^ expected struct `errors::ErrorB`, found struct `errors::ErrorA`
    |
    = note: expected type `errors::ErrorB`
               found type `errors::ErrorA`
Enter fullscreen mode Exit fullscreen mode

The error is clear. We should have used a ErrorB. We supplied an ErrorA instead. In general terms:

When chaining futures, the first function error type must be the same as the chained one.

That's exactly what rustc is telling us. The chained function return a ErrorB so the first function must return a ErrorB too. It does not (it returns a ErrorA instead) so the compilation fails.

How can we handle this problem? Fortunately there is a chainable method called map_err we can use. In our example we want to convert the ErrorA in ErrorB so we inject this function call between the two futures:

let future = fut_error_a()
    .map_err(|e| {
        println!("mapping {:?} into ErrorB", e);
        ErrorB::default()
    })
    .and_then(|_| fut_error_b());

let retval = reactor.run(future).unwrap_err();
println!("error chain == {:?}", retval);
Enter fullscreen mode Exit fullscreen mode

If we compile now and run the sample this will be the output:

mapping ErrorA into ErrorB
error chain == ErrorB
Enter fullscreen mode Exit fullscreen mode

Let's push the example further. Suppose we want to chain ErrorA, then ErrorB and then ErrorA again. Something like:

let future = fut_error_a()
    .and_then(|_| fut_error_b())
    .and_then(|_| fut_error_a());
Enter fullscreen mode Exit fullscreen mode

The rule clearly works on pairs. It does not account for the whole composition. So we have to map the error in ErrorB between fut_error_a and fut_error_b and then the other way around: map ErrorA between fut_error_b and fut_error_a. Hideous but working:

let future = fut_error_a()
    .map_err(|_| ErrorB::default())
    .and_then(|_| fut_error_b())
    .map_err(|_| ErrorA::default())
    .and_then(|_| fut_error_a());
Enter fullscreen mode Exit fullscreen mode

"From" to the rescue

One way to simplify the above code is to exploit the std::convert::From trait. Basically with the From trait you can tell Rust how to convert one struct into another automagically. The trait consumes the original error which is fine in our implementation. Also the trait assumes the conversion does not fail. Lastly, we can only implement traits for structs we own - ie written by us - so this approach cannot always be used.

Let's implement From<ErrorA> for ErrorB and FromInto<ErrorB> for ErrorA:

impl From<ErrorB> for ErrorA {
    fn from(e: ErrorB) -> ErrorA {
        ErrorA::default()
    }
}

impl From<ErrorA> for ErrorB {
    fn from(e: ErrorA) -> ErrorB {
        ErrorB::default()
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the previous code can be simplified by just calling the from_err() function instead of map_err(). Rust will be smart enough to figure out which conversion to use on its own:

let future = fut_error_a()
   .from_err()
   .and_then(|_| fut_error_b())
   .from_err()
   .and_then(|_| fut_error_a());
Enter fullscreen mode Exit fullscreen mode

The code is still intermingled with error conversion but the conversion code is no longer inline. This helps the readability of your code considerably.
The Future crate is clever: the from_err code will be called only in case of errors so there is no runtime penalty of using it.

Lifetimes

Rust signature feature is the explicit lifetime annotation of references. Most of the time, however, Rust allows us to avoid to specify the lifetime using lifetime elision. Let's see it in action. We want to code a function that takes a string reference and return, if successful, the same string reference:

fn my_fn_ref<'a>(s: &'a str) -> Result<&'a str, Box<Error>> {
    Ok(s)
}
Enter fullscreen mode Exit fullscreen mode

Notice the <'a> part. We are declaring a lifetime. Then with s: &'a str we are creating a parameter that will accept string references that must be valid for as long as 'a is valid. With the Result<&' str, Box<Error>> we are telling Rust that our return value will contain a string reference. That string reference must be valid as long as 'a is valid. In other words the passed string reference and the returned object must have the same lifetime.

That said this syntax is so verbose that Rust allows us to avoid specifying lifetimes in common case such as this. So we can rewrite the function this way:

fn my_fn_ref(s: &str) -> Result<&str, Box<Error>> {
    Ok(s)
}
Enter fullscreen mode Exit fullscreen mode

Notice there is no longer a mention to lifetimes, even though they are there. This function declaration is much simpler to understand at glance.

But... you cannot elide lifetimes when working with futures (yet). If you try to convert this function to the same impl Future one as we did in the previous blog post:

fn my_fut_ref_implicit(s: &str) -> impl Future<Item = &str, Error = Box<Error>> {
    ok(s)
}
Enter fullscreen mode Exit fullscreen mode

You will get an error. In my case (using rustc 1.23.0-nightly (2be4cc040 2017-11-01)) I've got a compiler panic:

   Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2)
error: internal compiler error: /checkout/src/librustc_typeck/check/mod.rs:633: escaping regions in predicate Obligation(predicate=Binder(ProjectionPredicate(ProjectionTy { substs: Slice([_]), item_def_id: DefId { krate: CrateNum(15), index: DefIndex(0:330) => futures[59aa]::future[0]::Future[0]::Item[0] } }, &str)),depth=0)
  --> src/main.rs:39:36
   |
39 | fn my_fut_ref_implicit(s: &str) -> impl Future<Item = &str, Error = Box<Error>> {
   |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

note: the compiler unexpectedly panicked. this is a bug.

note: we would appreciate a bug report: https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md#bug-reports

note: rustc 1.23.0-nightly (2be4cc040 2017-11-01) running on x86_64-unknown-linux-gnu

thread 'rustc' panicked at 'Box<Any>', /checkout/src/librustc_errors/lib.rs:450:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
Enter fullscreen mode Exit fullscreen mode

Don't fret on it, remember impl Future is still a nightly-only feature. To solve it we just have to avoid lifetime elision by explicitly annotating the references with lifetimes:

fn my_fut_ref<'a>(s: &'a str) -> impl Future<Item = &'a str, Error = Box<Error>> {
    ok(s)
}
Enter fullscreen mode Exit fullscreen mode

This will work as expected.

impl Future with lifetimes

The need to explicitly express the lifetimes goes beyond the parameters. If you return an impl Future constrained by a lifetime you have to annotate it as well. For example suppose we want to return a future String from a function taking a &str as parameter. We know we must annotate the parameter with lifetimes so we might try this:

fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> {
    my_fut_ref(s).and_then(|s| ok(format!("received == {}", s)))
}
Enter fullscreen mode Exit fullscreen mode

This will not work: the returned future is not constrained by 'a. The error is:

error[E0564]: only named lifetimes are allowed in `impl Trait`, but `` was found in the type `futures::AndThen<impl futures::Future, futures::FutureResult<std::string::String, std::boxed::Box<std::error::Error + 'static>>, [closure@src/main.rs:44:28: 44:64]>`
  --> src/main.rs:43:42
   |
43 | fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> {
   |                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

To solve the error all you have to do is to append 'a to the impl Future declaration, like this:

fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> + 'a {
    my_fut_ref(s).and_then(|s| ok(format!("received == {}", s)))
}
Enter fullscreen mode Exit fullscreen mode

Credit goes to HadrienG for this. See Trouble with named lifetimes in chained futures.

Now you can call the function using your reactor as usual:

let retval = reactor
    .run(my_fut_ref_chained("str with lifetime"))
    .unwrap();
println!("my_fut_ref_chained == {}", retval);
Enter fullscreen mode Exit fullscreen mode

And get the expected output:

my_fut_ref_chained == received == str with lifetime
Enter fullscreen mode Exit fullscreen mode

Closing remarks

In the next post we will cover the reactor. We will also write a future implementing struct from scratch.


Happy Coding,
Francesco Cogno

Top comments (2)

Collapse
 
cerceris profile image
Andrei Borodaenko • Edited

Hello again, Francesco. Thanks for this series of tutorials. And again, what is that?

FromInto<ErrorB>

Collapse
 
mindflavor profile image
Francesco Cogno

Whoops another typo!

Thank you for spotting this! 👍