`impl` vs generic bounds

I thought that impl in function signature is just a shortcut for where bound, but today I've discovered that my understanding is wrong. What is the difference between these 2 functions?

fn test1<F>() -> F where F: Future {
    async { }
}

fn test2() -> impl Future {
    async { }
}

The first one generates

error[E0308]: mismatched types
  --> src/bin/tool/main.rs:6:5
   |
5  | fn test1<F>() -> F where F: Future {
   |          -       - expected `F` because of return type
   |          |
   |          this type parameter
6  |     async { }
   |     ^^^^^^^^^ expected type parameter `F`, found opaque type
   | 
  ::: /home/vadym/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/future/mod.rs:61:43
   |
61 | pub const fn from_generator<T>(gen: T) -> impl Future<Output = T::Return>
   |                                           ------------------------------- the found opaque type
   |
   = note: expected type parameter `F`
                 found opaque type `impl Future`
   = help: type parameters must be constrained to match other types
   = note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters

impl has two different meanings in function signatures depending on whether it appears to the left of -> (argument position) or to the right of -> (return position):

  • impl A in argument position is equivalent to introducing an anonymous generic parameter X bounded by X: A (the usage you're familiar with)
  • impl A in return position means the function returns a specific type that is not concretely named, either because it doesn't have a name that you can write in code (like the opaque types that are generated by the compiler to represent async blocks) or because the author doesn't want to guarantee a specific name or other properties. The only thing callers know about the return type of an -> impl A function is that it implements A. (But the value is still Sized and calls to any trait functions are statically dispatched, unlike if you used dyn A.)

The reason your first signature isn't accepted is that the generic parameter F is universally quantified, so you're declaring "for every type that implements Future, test1 returns a value of that type". Obviously this is not a reasonable contract so you get a compiler error. To express "test1 returns a value of an anonymous type that implements Future", you need return-position impl Future—actually, return-position impl Future<Output = ()>, since you must specify each associated type for this usage.

4 Likes