Trying to understand generics

Hello,

In order to try to understand generics, I'm playing with the following:

fn main() {
    let val = test(5);
    println!("{:?}", val); // 5

    let val = test(true);
    println!("{:?}", val); // true
}

fn test<M>(value: M) -> M where M: Mixed {
    value
}

I can see that the compiler "knows" the output of test, since it is exactly the same as the input. I even can understand that the compiler maybe completely optimizes away the function at all.

But if I do the same with a struct, where the value is (temporarily) stored (which requires(?) boxing) it's a completely different story. I don even know if this is possible for the compiler to know what the value is at a certain moment of time:

fn main() {
    let mut test = Test::new(5);
    let val = test.get::<i64>();
    println!("{:?}", val);
}

struct Test {
    value: Box<dyn Mixed>,
}

impl Test {
    pub fn new<M>(value: M) -> Self where M: Mixed + 'static {
        Self {
            value: Box::new(value),
        }
    }

    pub fn get<M>(&mut self) -> M where M: Mixed + 'static {
        *self.value
    }
}

Which results in the following compiler error:

error[E0308]: mismatched types
  --> src/main.rs:50:9
   |
49 |     pub fn get<M>(&mut self) -> M where M: Mixed + 'static {
   |                -                - expected `M` because of return type
   |                |
   |                this type parameter
50 |         *self.value
   |         ^^^^^^^^^^^ expected type parameter `M`, found trait object `dyn Mixed`
   |
   = note: expected type parameter `M`
                found trait object `(dyn Mixed + 'static)`

Can someone explain me what happens here?

Thanks in advance,
Raymond

Okay, so first, no you do not need to box it to store it in the struct:

struct Test<M> {
    value: M,
}

impl<M> Test<M> {
    pub fn new(value: M) -> Self {
        Self {
            value,
        }
    }

    pub fn get(&self) -> &M {
        &self.value
    }
}
1 Like

One thing to keep in mind here is that the compiler only type-checks one function at a time, on purpose. Sometimes it will refuse to acknowledge things that seem obvious to a human, in order to be more predictable.

Your first example works because when type-checking main, the compiler sees 5 and true are passed to a function of type M -> M, and thus keep the same type even if the value changes. And when type-checking test, it sees that its input and output are the same type, by definition.

Your second example fails because the compiler is ignoring what's going on outside of Test::get. All it knows is that it self.value is a Box<dyn Mixed>- which could be absolutely any implementation of Mixed. Meanwhile, M is specified by the caller, so it doesn't know anything about that either.

So it tells you that dyn Mixed (any Mixed at all, chosen by whoever wrote value last) and M (also any Mixed at all, chosen by the caller) are different types. If you want to preserve the information that they're the same type, you might write something like @alice's example, which attaches M to Test instead of get.

If you do want to use dyn Mixed, you might need to resign yourself to working with it only via methods on Mixed. Or, you could downcast it using the Any trait, which forces you to handle the case where it's not the type you expected.

1 Like

So to understand the difference, the point is that every generic parameter introduces different functions/types for each choice of the parameter. If test is generic, then test<i32> is a different function from test<String>, and the compiler knows the return type is i32 because test<i32> has the return type i32.

Similarly if you have a generic struct Test, then Test<i32> is one type and Test<String> is some other type. In my snippet, each Test type has its own version of new and get, e.g. there's a Test<i32>::new that returns a Test<i32>, and there's also a different function Test<String>::new that instead returns a Test<String>. Similarly Test<String>::get returns a String and can only be used on a Test<String>.

On the other hand with the boxed trait object, you have thrown away the type information, and Test becomes a single type regardless of the underlying type. This means that Test::new<i32> and Test::new<String> both return the same type Test, and you can use Test::new<i32> to create a Test and then call Test::get<String> on that value. This is the source of your error.

1 Like

When you have get<M>, then the caller chooses the type M, and your function can be called with any type the caller wishes, so this:

Test::get::<String>(&Test::new::<bool>(true))

is valid Rust code per your definition. But your Box<dyn Mixed> was allowed to be cast to any M, then you would end up with bool used as String — obviously not a good idea.

@alice, @rpjohnst: Thank you so much explaining this in such detail! And my apologies for not replying sooner; in order for me to work on Rust I need some quiet-time to get focused, which I barely got last few months :frowning:

What you say makes sense and I'll play with it to see if I can fully understand what's happening!

Regards,
Raymond

1 Like

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.