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
}
}
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
}
}
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 {})
}
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);
The result is, unsurprisingly:
fut_error_a == ErrorA
fut_error_b == ErrorB
So far so good. Now let's try to chain these futures:
let future = fut_error_a().and_then(|_| fut_error_b());
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`
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);
If we compile now and run the sample this will be the output:
mapping ErrorA into ErrorB
error chain == ErrorB
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());
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());
"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()
}
}
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());
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)
}
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)
}
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)
}
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.
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)
}
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)))
}
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>> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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)))
}
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);
And get the expected output:
my_fut_ref_chained == received == str with lifetime
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)
Hello again, Francesco. Thanks for this series of tutorials. And again, what is that?
Whoops another typo!
Thank you for spotting this! π