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:
- Schedule the execution of
my_fut()
. - When
my_fut()
ends create a variable calledretval
and store into it the result ofmy_fut()
execution. - Now schedule the execution of
my_fn_squared(i: u32)
passingretval
as parameter. - 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
Top comments (9)
Why do you need the lambda?
why not?
You do not need the lambda in this case, your code will work just fine. I used the lambda for two reasons:
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:
The logical following step was to chain those statements into one, so I kept the same binding names (
retval
andretval2
):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: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:
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:
It will give, unsurprisingly:
You cannot curry the function either:
As Rust will complain like this:
For this reason I prefer the closures: the above code can be expressed like this:
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).
Hi Francesco. Thanks for the tutorial!
Could you please explain why do you use
my_fn_squared
and notmy_fut_squared
in the code below?Hi Andrei,
although there is no difference in the result between these two:
The signature is different:
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
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)
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 :))!
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
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.
Thank you so much.❤️