`impl` Trait Syntax

Hi, I watched Jonhoo's Crust of Rust on closures today and am a little confused about traits.
Given the following code:

pub fn linear1() -> impl Fn(f64) -> f64 {
    |x| x
}
pub fn linear2<F: Fn(f64) -> f64>() -> F {
    |x| x
}
pub fn linear3<F>() -> F
where
    F: Fn(f64) -> f64,
{
    |x| x
}

Only linear1 compiles. linear2 and linear3 fail with the following error:

error[E0308]: mismatched types
  --> codag/src/math.rs:42:9
   |
41 |     pub fn linear2<F: Fn(f64) -> f64>() -> F {
   |                    - this type parameter   - expected `F` because of return type
42 |         |x| x
   |         ^^^^^ expected type parameter `F`, found closure
   |
   = note: expected type parameter `F`
                     found closure `[closure@codag/src/math.rs:42:9: 42:14]`
   = help: every closure has a distinct type and so could not always match the caller-chosen type of parameter `F`

Why don't the last two functions compile? I thought that the impl syntax was just syntactical sugar for the other two.

I found the following in a reply on this thread:

  • In a return type, it’s an existential type that’s guaranteed to satisfy the listed trait bounds, but is otherwise opaque.

but I don't understand it.

1 Like

impl Trait in return position has a meaning that is different from its meaning in argument position.

In argument position, it's basically equivalent with a generic type parameter. But this is not the point of impl Trait, and you should probably just ignore it for the purposes of this discussion. I think impl Trait in parameter position was a mistake that should not have been added to the language.

The real power of impl Trait shines when it is in return position. There, it is not equivalent with a generic type parameter.

You see, a generic type parameter is just that: a type that the caller of the function decides. It can be anything the caller pleases, as long as it satisfies the "bounds" (contract) specified in the function signature.

However, impl Trait in return position is not something the caller chooses. Since you are returning from a function something that was generated by that very function, it is chosen by the implementation (body) of the function itself. The caller can't modify it.

This is the reason why your examples 2 and 3 (which are completely equivalent) do not compile. Let's rewrite your example with concrete types and a slightly more approachable trait like Display:

pub fn linear2<T: Display>() -> T {
    String::new()
}

Okay, so this "should" compile, since String is Display, right? Wrong! Since T is a type parameter, it is chosen by the caller. What if someone were to call your function as linear2::<u32>()? Clearly, u32 is Display, so it should work – alas, the function body returns a String, not an unsigned integer! So what should happen in this case? It doesn't make sense to allow choosing a type by the outside world, when it is necessarily determined by the implementation of the function, because they will not match up.

12 Likes

That was a very clear explanation. Thank you!

Your confusion is understandable. impl Trait in return position specifically is not syntactic sugar for a bounded generic input type parameter. Instead, it means that your function has a single, concrete type that meets the given bounds, but that type is opaque to users of your function. Usually because you either don't want to name the type to maintain flexibility, or because you can't name the type at all (e.g. it is or involves a closure).

impl Trait in return position is not generic over that return position, unless it involves some named type parameter.


So what have you written? Your function is generic -- the type parameter (in <...>) is an input to the function, that the chooser gets to call, even though it's not attached to a parameter. So I, a user of your library, could write:

 fn main() {
    let _ = linear2::<fn(f64) -> f64>();
    //                ^^^^^^^^^^^^^^
    // A function pointer type that matches your trait bounds
}

But the body of your linear2 isn't returning the user-chosen type, it's returning a closure (which has an unnameable type).


If you had written impl Trait in a parameter position, something like

fn linear4(f: impl Fn(f64) -> f64) { ... }

that is indeed mostly sugar for

fn linear4<F: Fn(f64) -> f64>(f: F) { ... }

which is sugar for

fn linear4<F>(f: F) where F: Fn(f64) -> f64 { ... }

And clearly you have heard of this desugaring. However, this was (IMHO) a mistake; some people think or thought that the symmetry with returning impl Trait is somehow intuitive, even though they have these completely different meanings.

Diagnostics could be better here. Since this is a not-uncommon confusion, return-position impl trait should be suggested.

(Only mostly sugar because the type parameter can no longer be named, or turbo-fished.)

2 Likes

Thanks!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.