Building more intuition for `MyType<T>` and `MyTrait<U>`

To explain this, it may be useful to think about the "implements" relation. For each type/trait pair, either the type implements the trait, or it does not. When it does implement it, you can use the (type, trait) pair to look up the implementation, which gives you knowledge of anything inside the impl block (the methods, associated types, associated constants and so on). Notably, a type can only implement a given trait once.

How do generics come into the picture? Well, basically, you don't get a type until you've specified every single generic parameter. And you don't get a trait until you've specified every single generic parameter. So, Vec is not a type, but Vec<u8> is. Similarly, AsRef is not a trait, but AsRef<str> is. Once you've defined types and traits in this way, the explanation in the previous paragraph applies. A type such as Vec<u8> can choose to implement AsRef<str> or not. It can do this independently of the completely unrelated type Vec<String> or the completely unrelated trait AsRef<u8>.

Okay, maybe they're not completely unrelated. However, this is only true insofar as the ways in which the syntax for impl blocks limit you.

Understanding generic impl blocks is quite similar to generic structs or traits or methods. When something has a list of generic parameters, then you should duplicate that thing for every single possible combination of types that you can assign to the generic parameters, that satisfies the where clauses. So for example:

struct Vec<T> { ... }

is syntax-sugar to the infinite list:

struct Vec<u8> { ... }
struct Vec<String> { ... }
struct Vec<char> { ... }
struct Vec<TcpStream> { ... }
...

Similarly,

impl<T> AsRef<T> for Box<T> { ... }

is syntax-sugar for the following infinite list:

impl AsRef<u8> for Box<u8> { ... }
impl AsRef<String> for Box<String> { ... }
impl AsRef<char> for Box<String> { ... }
impl AsRef<TcpStream> for Box<TcpStream> { ... }
...

These duplications really are just "literally insert the type where it says T", nothing more, nothing less. You get "pass-through" when you use T on both sides. When you don't do that, you get orthogonal situations. When you use constructs like <T as Iterator>::Item on one side, then you get more complicated situations, since it expands to stuff like <Iter<'a, u8> as Iterator>::Item, which is long-hand for the type &'a u8.

As for associated types, they're different from generics. There's only one Iterator trait, and each implementation can make only one choice for what the Item associated type should be. The associated types are simply part of the information you look up when you look up the type/trait pair. This, combined with the fact that a type can only implement a trait once, is why expressions like <T as Iterator>::Item are meaningful. They perform a type/trait pair lookup and return the value of the associated type called Item.

Finally, as a note on lifetimes, they are completely like types here. The references &'a str and &'b str are different types when the lifetimes are different, and &str is not a type, but a type constructor that takes a lifetime and returns a type.

Yes, I find this a misleading way to explain it. Traits are not types. Trait objects are types, but they're a different thing from the trait they're associated with.

11 Likes