DEV Community

Francesco Cogno
Francesco Cogno

Posted on • Updated on

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

Intro

If you are a programmer and you like Rust you might be aware of the future movement that is buzzing around the Rust community. Great crates have embraced it fully (for example Hyper) so we must be able to use it. But if you are an average programmer like me you may have an hard time understanding them. Sure there is the official Crichton's tutorial but, while being very thorough, I've found it very hard to put in practice.

I'm guessing I'm not the only one so I will share my findings hoping they will help you to become more proficient with the topic.

Futures in a nutshell

Futures are peculiar functions that do not execute immediately. They will execute in the future (hence the name). There are many reasons why we might want to use a future instead of a standard function: performance, elegance, composability and so on. The downside is they are hard to code. Very hard. How can you understand causality when you don't know when a function is going to be executed?

For that reason programming languages have always tried to help us poor, lost programmers with targeted features.

Rust's futures

Rust's future implementation, true to its meme, is a work in progress. So, as always, what are you reading can already be obsolete. Handle with care.

Rust's futures are always Results: that means you have to specify both the expected return type and the alternative error one.

Let's pick a function and convert it to future. Our sample function returns either u32 or a Boxed Error trait. Here it is:

fn my_fn() -> Result<u32, Box<Error>> {
    Ok(100)
}

Very easy. Not look at the relative future implementation:

fn my_fut() -> impl Future<Item = u32, Error = Box<Error>> {
    ok(100)
}

Notice the differences. First the return type is no longer a Result but a impl Future. That syntax (still nightly-only at the time of writing) allows us to return a future. The <Item = u32, Error = Box<Error>> part is just a more verbose way of detailing the returned type and the error type as before.

You need the conservative_impl_trait nightly feature for this to work. You can return a boxed trait instead but it's more cumbersome IMHO.

Notice also the case of the Ok(100) returned value. In the Result function we use the capitalized Ok enum, in the future one we use the lowercase ok method.

Rule of the thumb: use lowercase return methods in futures.

All in all is not that bad. But how can we execute it? The standard function can be called directly. Note that since we are returning a Result we must unwrap() to get the inner value.

let retval = my_fn().unwrap();
println!("{:?}", retval);

As futures return before actually executing (or, more correctly, we are returning the code to be executed in the future) we need a way to run it. For that we use a Reactor. Just create it and call its run method to execute a future. In our case:

let mut reactor = Core::new().unwrap();

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

Notice here we are unwrapping the run(...) return value, not the my_fut() one.

Really easy.

Chaining

One of the most powerful features of futures is the ability to chain futures together to be executed in sequence. Think about inviting your parents for dinner via snake mail. You send them a mail and then wait for their answer. When you get the answer you proceed preparing the dinner for them (or not, claiming sudden crippling illness). Chaining is like that. Let's see an example.

First we add another function, called squared both as standard and future one:

fn my_fn_squared(i: u32) -> Result<u32, Box<Error>> {
    Ok(i * i)
}


fn my_fut_squared(i: u32) -> impl Future<Item = u32, Error = Box<Error>> {
    ok(i * i)
}

Now we might call the plain function this way:

let retval = my_fn().unwrap();
println!("{:?}", retval);

let retval2 = my_fn_squared(retval).unwrap();
println!("{:?}", retval2);

We could simulate the same flow with multiple reactor runs like this:

let mut reactor = Core::new().unwrap();

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

let retval2 = reactor.run(my_fut_squared(retval)).unwrap();
println!("{:?}", retval2);

But there is a better way. A future, being a trait, has many methods (we will cover some here). One, called and_then, does exactly what we coded in the last snippet but without the explicit double reactor run. Let's see it:

let chained_future = my_fut().and_then(|retval| my_fn_squared(retval));
let retval2 = reactor.run(chained_future).unwrap();
println!("{:?}", retval2);

Look at the first line. We have created a new future, called chained_future composed by my_fut and my_fut_squared.

The tricky part is how to pass the result from one future to the next. Rust does it via closure captures (the stuff between pipes, in our case |retval|). Think about it this way:

  1. Schedule the execution of my_fut().
  2. When my_fut() ends create a variable called retval and store into it the result of my_fut() execution.
  3. Now schedule the execution of my_fn_squared(i: u32) passing retval as parameter.
  4. Package this instruction list in a future called chained_future.

The next line is similar as before: we ask the reactor to run chained_future instruction list and give us the result.

Of course we can chain an endless number of method calls. Don't worry about performance either: your future chain will be zero cost (unless you Box for whatever reason).

The borrow checked may give you an hard time in the capture chains. In that case try moving the captured variable first with move.

Mixing futures and plain functions

You can chain futures with plain functions as well. This is useful because not every function needs to be a future. Also you might want to call external function over which you have no control.

If the function does not return a Result you can simply add the function call in the closure. For example, if we have this plain function

fn fn_plain(i: u32) -> u32 {
    i - 50
}

Our chained future could be:

let chained_future = my_fut().and_then(|retval| {
    let retval2 = fn_plain(retval);
    my_fut_squared(retval2)
});
let retval3 = reactor.run(chained_future).unwrap();
println!("{:?}", retval3);

If your plain function returns a Result there is a better way. Let's chain our my_fn_squared(i: u32) -> Result<u32, Box<Error>> function.

You cannot call and_then to a Result but the futures create have you covered! There is a method called done that converts a Result into a impl Future.
What does that means to us? We can wrap our plain function in the done method and than we can treat it as if it were a future to begin with!

Let's try it:

let chained_future = my_fut().and_then(|retval| {
    done(my_fn_squared(retval)).and_then(|retval2| my_fut_squared(retval2))
});
let retval3 = reactor.run(chained_future).unwrap();
println!("{:?}", retval3);

Notice the second line: done(my_fn_squared(retval)) allows us to chain a plain function as it were a impl Future returning function. Now try without the done function:

let chained_future = my_fut().and_then(|retval| {
    my_fn_squared(retval).and_then(|retval2| my_fut_squared(retval2))
});
let retval3 = reactor.run(chained_future).unwrap();
println!("{:?}", retval3);

The error is:

   Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2)
error[E0308]: mismatched types
   --> src/main.rs:136:50
    |
136 |         my_fn_squared(retval).and_then(|retval2| my_fut_squared(retval2))
    |                                                  ^^^^^^^^^^^^^^^^^^^^^^^ expected enum `std::result::Result`, found anonymized type
    |
    = note: expected type `std::result::Result<_, std::boxed::Box<std::error::Error>>`
               found type `impl futures::Future`

error: aborting due to previous error

error: Could not compile `tst_fut2`.

The expected type std::result::Result<_, std::boxed::Box<std::error::Error>> found type impl futures::Future is somewhat misleading. In fact we were supposed to pass an impl Future and we used a Result instead (not the other way around). This kind of confusion is common using futures (see https://github.com/alexcrichton/futures-rs/issues/402). We'll talk about it in the second part of the post.

Generics

Last but not least, futures work with generics without any magic.

Let's see an example:

fn fut_generic_own<A>(a1: A, a2: A) -> impl Future<Item = A, Error = Box<Error>>
where
    A: std::cmp::PartialOrd,
{
    if a1 < a2 {
        ok(a1)
    } else {
        ok(a2)
    }
}

This function returns the smaller parameter passed. Remember, we need to give and Error even if no error is expected in order to implement the Future trait. Also, the return values (ok in this case) are lowercase (the reason is ok being a function, not an enum).

Now we can run the future as always:

let future = fut_generic_own("Sampdoria", "Juventus");
let retval = reactor.run(future).unwrap();
println!("fut_generic_own == {}", retval);

I hope you got the gist by now :). Until now it should make sense. You may ave noticed I avoided using references and used owned values only. That's because lifetimes do not behave the same using impl Future. I will explain how to use address them in the next post. In the next post we will also talk about error handling in chains and about the await! macro.

Notes

If you want to play with these snippets you just need to add in your Cargo.toml file:

[dependencies]
futures="*"
tokio-core="*"
futures-await = { git = 'https://github.com/alexcrichton/futures-await' }

And put these lines on top of src/main.rs:

#![feature(conservative_impl_trait, proc_macro, generators)]

extern crate futures_await as futures;
extern crate tokio_core;

use futures::done;
use futures::prelude::*;
use futures::future::{err, ok};
use tokio_core::reactor::Core;
use std::error::Error;
The futures-await crate is not really needed for this post but we will use it in the next one.

Happy coding,
Francesco Cogno

Latest comments (9)

Collapse
 
vthg2themax profile image
Vince Pike

Given that futures are part of standard now, so you think learning tokio will still be a useful skill or should one spend their energy trying to figure out the new standard? (Which looks pretty complex)

Collapse
 
mindflavor profile image
Francesco Cogno

Most of the stuff above still applies to the "new" features (but there are some differences like the Pin type) so it can be useful to read it, especially because combinators are still allowed.

That said, the await syntax is the future so you will have to study it anyway. You might as well start from there and save some time.
It's still Rust though: what seems complex usually is! I plan to write about the new standard when the dust settles (and when I'm knowledgeable about it, which may never happen :))!

Collapse
 
sreyassreelal profile image
__SyS__

Thank you so much.❤️

Collapse
 
cerceris profile image
Andrei Borodaenko

Hi Francesco. Thanks for the tutorial!
Could you please explain why do you use my_fn_squared and not my_fut_squared in the code below?

let chained_future = my_fut().and_then(|retval| my_fn_squared(retval));
Collapse
 
mindflavor profile image
Francesco Cogno

Hi Andrei,
although there is no difference in the result between these two:

let chained_future = my_fut().and_then(|retval| my_fn_squared(retval));

let chained_future = my_fut().and_then(|retval| my_fut_squared(retval));

The signature is different:

futures::AndThen<impl futures::Future, std::result::Result<u32, std::boxed::Box<dyn std::error::Error>>, [closure@src/main.rs:25:45: 25:75]>

futures::AndThen<impl futures::Future, impl futures::Future, [closure@src/main.rs:30:45: 30:76]>

That said I think you are right, the code I used in the post is misleading. I will correct it right away!

Thank you very much,
Francesco

Collapse
 
jnordwick profile image
Jason Nordwick • Edited

Why do you need the lambda?

let chained_future = my_fut().and_then(|retval| my_fn_squared(retval));

why not?

let chained_future = my_fut().and_then(my_fn_squared);
Collapse
 
mindflavor profile image
Francesco Cogno • Edited

You do not need the lambda in this case, your code will work just fine. I used the lambda for two reasons:

  1. Blog post logic
  2. Global ergonomics (in other words, taste :))

Blog post logic

In the post I was migrating step-by-step from a non-async code to an async one. In the previous step I had this code:

let mut reactor = Core::new().unwrap();

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

let retval2 = reactor.run(my_fut_squared(retval)).unwrap();
println!("{:?}", retval2);

The logical following step was to chain those statements into one, so I kept the same binding names (retval and retval2):

let chained_future = my_fut().and_then(|retval| my_fn_squared(retval));
let retval2 = reactor.run(chained_future).unwrap();
println!("{:?}", retval2);

IMHO hiding the retval value would have been harder to follow.

Taste

I generally prefer to use closures as they are more explicit. Let me draft an example.
If we take into account these three Future returning functions:

fn my_fut() -> impl Future<Item = u32, Error = Box<Error>> {
    ok(100)
}

fn my_fut_squared(i: u32) -> impl Future<Item = u32, Error = Box<Error>> {
    ok(i * i)
}

fn my_fut_two(i: u32, j: u32) -> impl Future<Item = u32, Error = Box<Error>> {
    ok(i + j)
}

The first one takes no parameters, the second one just one and the third one two.

You can chain the first two as you did. That's the functional approach:

let chained_future_simple = my_fut().and_then(my_fn_squared);
let retval2 = reactor.run(chained_future_simple).unwrap();
println!("chained_future_simple == {:?}", retval2);

That is, the parameter is implicitly passed between the futures. But what about chaining the first and the third future? This will not work because the third future expects two parameters:

let chained_future_two = my_fut().and_then(my_fut_two);
let retval2 = reactor.run(chained_future_two).unwrap();
println!("chained_future_two == {:?}", retval2);

It will give, unsurprisingly:

error[E0593]: function is expected to take 1 argument, but it takes 2 arguments
   --> src/main.rs:141:27
    |
141 |     let retval2 = reactor.run(chained_future_two).unwrap();
    |                           ^^^ expected function that takes 1 argument
    |
    = note: required because of the requirements on the impl of `futures::Future` for `futures::AndThen<impl futures::Future, _, fn(u32, u32) -> impl futures::Future {my_fut_two}>`

You cannot curry the function either:

let chained_future_two = my_fut().and_then(my_fut_two(30));
let retval2 = reactor.run(chained_future_two).unwrap();
println!("chained_future_two == {:?}", retval2);

As Rust will complain like this:

error[E0061]: this function takes 2 parameters but 1 parameter was supplied
   --> src/main.rs:140:59
    |
31  | / fn my_fut_two(i: u32, j: u32) -> impl Future<Item = u32, Error = Box<Error>> {
32  | |     ok(i + j)
33  | | }
    | |_- defined here
...
140 |       let chained_future_two = my_fut().and_then(my_fut_two(30));
    |                                                             ^^ expected 2 parameters

For this reason I prefer the closures: the above code can be expressed like this:

let chained_future_two = my_fut().and_then(|retval| my_fut_two(retval, 30));
let retval2 = reactor.run(chained_future_two).unwrap();
println!("chained_future_two == {:?}", retval2);

It's more verbose but in this case I like the explicitness more. Also Rust gives you control if you want to move the captured variables so you have control over that too.
In the end I think it's a matter of taste (I don't know if there is speed difference so maybe one solution is better than the other in that regard).

Collapse
 
thesirc profile image
King Claudy

Thank you for taking the time to write about futures! The post is really good and meets his expectations. May be you should consider writing a part of The Book on futures with the help of Alex Crichton

Collapse
 
mindflavor profile image
Francesco Cogno

Thank you for reading it! I will contact Alex Crichton and see if it makes sense to include it in the book.

The point is I would like these posts to be more relaxed in terms of both writing style and lexical accuracy: Rust tends to be intimidating as it is :). Take the official future tutorial for example: it's technically excellent but it starts with the why instead of how. An average programmer, such as me, might want to start with small steps instead of being thrown directly into the intricacies of the asynchronous programming.