How should I understand this code?

Why can func1, func2, and func3 run correctly, but func4 will report an error?

fn main() {
    #[allow(dead_code)]
    struct Sheep;

    trait Animal {
        fn new() -> Self;
    }

    impl Animal for Sheep {
        fn new() -> Self {
            Sheep
        }
    }

    fn func1() -> impl Animal {
        Sheep
    }

    fn func2<T : Animal>(t : T) -> T {
        t
    }

    fn func3<T : Animal>() -> T {
        T::new()
    }

    fn func4<T : Animal>() -> T {
        Sheep
    }
}

ERROR:

error[E0308]: mismatched types
  --> src\main.rs:28:9
   |
27 |     fn func4<T : Animal>() -> T {
   |              -                -
   |              |                |
   |              |                expected `T` because of return type
   |              |                help: consider using an impl return type: `impl Animal`
   |              this type parameter
28 |         Sheep
   |         ^^^^^ expected type parameter `T`, found `Sheep`
   |
   = note: expected type parameter `T`
                      found struct `Sheep`

For more information about this error, try `rustc --explain E0308`.

Please always post the error.

Think about what would happen if the caller passed in a Frog, which would implement Animal.

5 Likes

T is not guaranteed to equal Sheep. In Rust functions can only return a single type.

I think the error message is inadequate due to number of times similar posts have appeared.
Question is; what is needed to improve it?

In func1, the callee (function author) chooses one specific,but opaque, type to return (which must implement Animal). Being opaque, the caller can only count on it implementing Animal[1].

In all the other cases, the caller chooses the type of T (which must implement Animal), and can even choose differently at each call site, and the function has to accommodate any and every possible choice. It can only count on it implementing Animal.


The return of func1 is called an RPIT - return position impl trait. There's also APIT - argument position impl trait. Unfortunately, impl trait has a very different meaning in argument position - it's effectively an anonymous type parameter to the function and thus acts like the last three example functions.


  1. and leaked autotraits ↩︎

2 Likes

The signature fn …<T : Animal>() -> T means “you tell me an animal type T, and I give you a value of that type”

The implementation func3 does this. Regardless if you call func3::<Sheep>() or func3::<Cow>() or whatever other types might implement Animal, you always get an animal of that type. func3::<Sheep>() returns a Sheep value, func3::<Cow>() returns a Cow value, etc…

However func4 tries to always return a Sheep. If you call func4::<Cow>(), returning Sheep is simply returning a value of the wrong type.


As for the other signatures… fn…() -> impl Animal means, “you call me, I choose a some animal type and return a value of that type”. There are a few more caveats regarding on what input exactly this choice of return type the function makes can depend. (Answer: It cannot depend on any run-time logic, but it can depend on generic type parameters (and const generics), and on lifetimes[1].) The function func1 thus chooses the animal type Sheep and returns a value of that type, which matches what the signature promises.

And fn…<T : Animal>(t : T) -> T means “you tell me an animal type T, and you pass me a value of that type, and I return you a value of that (same) type”. The function func2 thus accepts a value of the type T the caller chose, and just returns that same value, which matches what the signature promises.


  1. but lifetime only if they are syntactically mentioned in the “…something…” part of the -> impl …something… return type ↩︎

2 Likes

Nah, it's just that people aren't accustomed to generic programming (and apparently don't read the Book carefully enough) :man_shrugging:

The error message is perfectly fine. It's the same error that you would get if you tried to return a String while a u64 was specfied as the return type. It's the exact same reason, too.

Yeah I guess people might have a problem with the concept that a type Parameter T: SomeTrait represent an arbitray but fixed type from the set {T: T implements SomeTrait}. I remember how I struggled with this concept of "arbitrary but fixed" in my first year at university. Generics seem like they could represent any type at once but they don't. They represent one fixed type per invocation and that's a subtle distinction for a beginner. So they get confused when the error tells them that T and Sheep aren't the same type, because it seems like T should just be able to be a Sheep because it is generic.

1 Like

I guess calling it "arbitrary but fixed" is also missing the point. Return-position impl Trait is also an "arbitrary but fixed" type in a sense, yet the example would compile perfectly fine using RPIT.

The essential distinction is that a generic type parameter is chosen by the caller. Therefore nothing inside the body of the callee can influence what it is. A generic type parameter can be freely manipulated by the caller but it must be accepted by the implementation as-is.

I don't think this has anything to do with the type being "fixed"; it's the exact same issue as the value-level problem of trying to mutate a variable in the callee by writing to a by-value (moved/copied) argument. It's simply not how the semantics of the language is defined.

1 Like

There may be a form of survivorship bias here. We don't know the number of people who are completely fine with this error message since they won't report.

3 Likes

I wasn't talking about the distinction between the examples. I was trying to find an explanation why the error message doesn't seem to be understood by some people. Actually I had the exact mental blockade I described so just saying it is wrong is kind of dismissive.

Looks like you are discussing the important, but different, meta-questions:

  1. Is it desirable to make error messages understandable by people who have wrong mental model of how Rust works?
  2. Is it even possible to make error messages understandable by people who have wrong mental model of how Rust works?

You say that #1 is an important thing, while @H2CO3 says that answer to #2 is “no”, which means answer to #1 is not even worth discussing.

I'm not sure how often the rustc --explain E0308 feature is used, but I think it's a good place for more detailed information without adding clutter to the compiler's output. The current explanation doesn't talk about generic functions, it may be useful to expand it:

Expected type did not match the received type.

Erroneous code examples:

fn plus_one(x: i32) -> i32 {
    x + 1
}

plus_one("Not a number");
//       ^^^^^^^^^^^^^^ expected `i32`, found `&str`

if "Not a bool" {
// ^^^^^^^^^^^^ expected `bool`, found `&str`
}

let x: f32 = "Not a float";
//     ---   ^^^^^^^^^^^^^ expected `f32`, found `&str`
//     |
//     expected due to this

This error occurs when an expression was used in a place where the compiler expected
an expression of a different type. It can occur in several cases, the most
common being when calling a function and passing an argument which has a different
type than the matching type in the function declaration

Worst case scenario it would at least be something we could point at when people face this issue.

No I didn't really. I just tried to give a possible explanation as to why people might not understand the error message. I'm not saying that anything needs to be done about it.

Perhaps I can understand it as follows: Generics are arbitrary when monomorphic and fixed when non monomorphic.

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.