Minimal Futures examples demonstrating confusing compiler errors


#1

Background: I am a relatively new Rust programmer (started less than half a year ago) who just recently began working on a project where I had to learn the Futures framework.

While learning the Futures framework, I’ve been struggling a lot with confusing compiler error messages. And when I run into one of these error messages, Googling it along with the keywords “rust futures” doesn’t yield similar examples or solutions.

Where I have found success has been taking working examples, making changes which I know will break it, and seeing the resulting compiler error. Furthermore, converting these examples into their equivalent Result version demonstrates just how confusing the Future error messages are.

A simple Future example

Consider the following method, which doesn’t compile:

fn future_sign(v: i32) -> Box<Future<Item=i32, Error=i32> >{
    let f = ok(v)
           // this 'map' should be 'and_then'
           .map(|v| {
                // OR the returns should be raw i32, instead of ok(i32)
               if v > 0 {
                  ok(1)
               } 
               else {
                  ok(-1)
               } 
            });
    // The error message will point to the line below:
    Box::new(f)
}

map will return a Future type, but because we are returning futures instead of the underlying items in the closure, the type of f is actually Future<Item = Future<i32, _>, Error = _>. This can’t be coerced into the required return type, and so we would expect an error:

error[E0271]: type mismatch resolving `<[closure@src/main.rs:8:17: 16:14] as std::ops::FnOnce<(i32,)>>::Output == i32`
  --> src/main.rs:18:5
   |
18 |     Box::new(f)
   |     ^^^^^^^^^^^ expected struct `futures::FutureResult`, found i32
   |
   = note: expected type `futures::FutureResult<{integer}, _>`
              found type `i32`
   = note: required because of the requirements on the impl of `futures::Future` for `futures::Map<futures::FutureResult<i32, i32>, [closure@src/main.rs:8:17: 16:14]>`
   = note: required for the cast to the object type `futures::Future<Error=i32, Item=i32>`

error: aborting due to previous error

The error message helps hone in on the problem: it can’t resolve the type of the closure in map to be FnOnce(i32) -> i32, but the explanation which follows is misleading. The explanation at least points at the right line, where we wrap the future result in Box and return it (since this is where the type inference first finds an inconsistency), but the reason why is unhelpful. I would think that the type signature of future_sign takes precedence over the inference of the type of f, and that it would complain that Box(f), which is Box<Future<Future<i32, _>, _> doesn’t line up with the expected Box<Future<i32, i32>>. But instead, it is complaining that it is expecting a FutureResult and got an i32 instead. What’s even more confusing is that it is pointing to the entire line, including the Box call, instead of just the parameter f. For a newbie, you can see how trying to make sense of the error explanation will just confuse you more.

You can play with this example here:
https://play.rust-lang.org/?gist=2b5b8ebf139343333225960d4b0f3b55&version=stable&mode=debug&edition=2015

The analogous Result example

It’s worth noting that the analogous example with Result doesn’t have this problem:

fn result_sign(v: i32) -> Box<Result<i32, i32>> {
    let r = Ok(v)
           // this 'map' should be 'and_then'
           .map(|v| {
                // OR the returns should be raw i32, instead of Ok(i32)
               if v > 0 {
                  Ok(1)
               } 
               else {
                  Ok(-1)
               } 
            });
    // The error message will point to the line below:
    Box::new(r)
 }

This is the exact same problem, but with all Future types replaced with Result. The resulting compiler error is:

error[E0308]: mismatched types
  --> src/main.rs:15:14
   |
15 |     Box::new(r)
   |              ^ expected i32, found enum `std::result::Result`
   |
   = note: expected type `std::result::Result<i32, i32>`
              found type `std::result::Result<std::result::Result<{integer}, _>, _>`

which correctly informs that the type of r should be Result<i32, i32>, not Result<Result<i32, _>, _>. Admittedly, it has a somewhat similar issue, where it points to r and says it expected i32, found enum `std::result::Result`. But at least the explanation helps clarify the issue.
You can play with this example here:
https://play.rust-lang.org/?gist=7a1e60d8f338ae9f3cff25dbf8621615&version=stable&mode=debug&edition=2015

More examples

Although in this post I mostly just point out how bad the Futures error messages are, my original motivation for this was for learning purposes, to give myself some minimal examples of the kinds of errors you can get. That way, as I run into these in more complicated Futures code, I may be able to take the compiler error and relate it to an error from a small example. If others have similar small examples that demonstrate how some confusing errors can come up, I think they would be helpful, both for myself and others who find themselves learning Futures. And if the analogous Result variation gives more straight forward errors, it’s another example of how Futures compiler errors are unnecessarily confusing.


#2

I think that by allowing people to stop directly manipulating futures object when it is not necessary, async/await will also improve the ergonomics of these use cases.


#3

Wow, what a great way to learn something, and sharing it back too! :heart:

I find your observation that the errors are so wildly different between the Future and Result versions very insightful.
I personally haven’t played with futures enough to understand why this would be, but I’d be very interested if the (apparently spot-on) Result-error-logic can somehow be “ported” to also apply to the futures-case.

Rust considers “unhelpful error messages” to be a first-class bug, and this looks like we could improve the error message using already-existing logic. (Hopefully a lot less effort than the full “proper fix” of async/await)


#4

To this day, I find the “expected type X, found type Y” messages confusing and backwards, for the reason you mention. These occur in contexts other than futures so is a more general issue. If you search rust’s github issues, I believe you’ll see reports of this in various incarnations.

As for async/await, it’ll help some but there will still be a need for using combinators and I don’t think it’ll help there.


#5

My initial guess is that its because Result is a concrete type, whereas Future is a trait. Because its a trait, we’re always talking about it at one level of remove - either in terms of "type which implements Future", or more abstract things like a Box<Future<_>> trait object.

I wonder what the impl Future<> variant looks like…
(https://play.rust-lang.org/?gist=2278c7df0f5fa9cd04fe7b87a20d79f4&version=stable&mode=debug&edition=2015)

Very similar, except that it has a spurious complaint about the type being unsized:

error[E0271]: type mismatch resolving `<[closure@src/main.rs:7:17: 15:14] as std::ops::FnOnce<(i32,)>>::Output == i32`
 --> src/main.rs:4:27
  |
4 | fn future_sign(v: i32) -> impl Future<Item=i32, Error=i32> {
  |                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `futures::FutureResult`, found i32
  |
  = note: expected type `futures::FutureResult<{integer}, _>`
             found type `i32`
  = note: required because of the requirements on the impl of `futures::Future` for `futures::Map<futures::FutureResult<i32, i32>, [closure@src/main.rs:7:17: 15:14]>`
  = note: the return type of a function must have a statically known size