Why dyn cannot infer associated types when impl can?

I noticed (thanks to compile errors) that you may not need to specify a trait's associated type when you use impl but with almost the same code using dyn you need to spell out all the associated types.

In the sample code below, both struct A and struct B implements trait Y. struct A's method returns a impl Y, while struct B's method returns a Box<dyn Y>. Without specifying the associated type Y1, A compiles, but not B. To me, they both look exactly (almost) the same, but for struct B, somehow the compiler can't infer the type of Y1?

I would like to understand why? and what's the difference?
(i edited to simplify the sample)

trait Y {
    type Y1;

    fn do_y(&mut self) -> Self::Y1;
}

impl Y for String {
    type Y1 = String;

    fn do_y(&mut self) -> Self::Y1 { "Hi".to_string() }
}

struct A {}
impl A {
    // Compiles OK, Compiler infers? Y::Y1 as string
    pub fn do_a() -> impl Y { "hello".to_string() }
}

struct B {}
impl B {
    // Compiles FAIL, Why can't the compiler infer Y::Y1 as string
    pub fn do_b() -> Box<dyn Y> {
        Box::new("hello".to_string() )
    }
}

Compile error for impl B:

error[E0191]: the value of the associated type `Y1` (from trait `Y`) must be specified
  --> src/traiterrors2.rs:24:30
   |
4  |     type Y1;
   |     -------- `Y1` defined here
...
24 |     pub fn do_b() -> Box<dyn Y> {
   |                              ^ help: specify the associated type: `Y<Y1 = Type>`

You can only erase the Self type; you cannot erase associated types. dyn Y is not a proper¹ trait object type because you can't use the trait without knowing what Y1 is. Thus when you erase Self you have to specify what Y1 is: dyn Y<Y1 = String>.

One way to understand this is to consider vtable compatibility. dyn Trait implements Trait in the form of a vtable that contains pointers to functions. But you can't call a function if you don't know its signature, and so although vtables that implement dyn Y<Y1 = String> are all compatible with each other, they're not compatible with vtables that implement dyn Y<Y1 = u32> (for instance). It would be nonsensical to lump them all in one category and call it dyn Y, because then you would never know what type to expect when calling do_y. But you can't just ignore do_y either, because dyn Trait always implements Trait - it must, in order to be fully general.

impl Y is different because the type is not erased at all. So Y1 is resolved to String, and if you query its size_of etc. you will find those things to be consistent with String. But it's still an opaque type (to the caller of do_a, that is); you wouldn't be able to write code like let s: String = do_a().do_y(); because do_a only promises to return something that implements Y, not something that implements Y<Y1 = String>.

The caller of do_a is effectively generic over the type impl Y returned by do_a. This is kind of backwards from how we usually think about type parameters, but it's not much different from defining a generic function that takes a <T: Y>. The exact type represented by T is always known by the compiler after monomorphization, but it's opaque inside the generic function. Similarly, the exact type represented by impl Y is always known by the compiler as well, but it's opaque outside do_a.

¹ "proper" is not the official term for this, I just made it up

4 Likes

A dyn Trait would need to know any associated types because (1) the different implementations may have different function signatures (taking an associated type as an argument or returning one), and (2) the different implementations each have their own vtable, and the compiler needs to know which one to use when coercing the underlying type into dyn Trait.

One could argue that if the types are unspecified at some point after coercion, Rust should just not let you use any method or other associated item involving the unspecified associated type. That's a future possibility, but I just don't think there's much demand for such behavior -- one normally needs to know the associated type, if a trait has one. If you wanted, though, you could make a sub-trait that does this yourself. (Edit: On second thought, not a sub-trait. Just another trait with a blanket implementation based on the original trait.)

To leave room for future possibilities, having to specify that there are associated types involved is just flat-out required for dyn Trait, even when the compiler can infer it. And it can infer it; this compiles with your example:

let x: Box<dyn Y<Y1 = _>> = Box::new(String::new());

However note that in your original example, the body of a function is purposefully not used to infer things that can't be determined from the signature, because the signature is also the contract for users of the function. If you try to use dyn Y<Y1 = _> in the signature, you'll get an error.


impl Trait in return position, on the other hand, is an opaque but shallow type alias. The type isn't erased and there are no vtables, the compiler just doesn't allow code that relies on the impl Trait to use anything that's not specified in the alias. So if you try to use the unspecified associated type in any way that's not bound by the trait declaration, you won't be able to.

5 Likes

Thanks for the explanation, helps clear up my understanding

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.