Why not `trait Iterator<Item>`?

The Iterator trait is defined as follows:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

Is there a reason it’s not just:

trait Iterator<Item> {
    fn next(&mut self) -> Option<Item>;
}

I think it used to be that way – there’s some discussion of Iterator in RFC 195 associated items.

2 Likes

The value of associated types is that they get the type parameters out of the type signature of the trait. You only have to specify those types when implementing the trait, and not when using the trait. This is covered in the book:

https://doc.rust-lang.org/book/ch19-03-advanced-traits.html

Here is a heuristic I use: if you expect a particular struct to impl a trait in only one way, the types associated with the trait should be associated types. If you think it’s useful to impl a trait multiple times for different type parameters, it should be a generic trait.

It’s pretty uncommon to want to implement a trait multiple times. For some traits, like conversion traits, yes it’s useful. But for many - probably the majority of traits - you don’t want or expect multiple implementations of it for a given type. So keeping your type signature smaller makes the code that uses the trait easier to read.

4 Likes

The reason is that an Iterator should only yield 1 type of item, not many types of items depending on how you call it (which generic parameters are being used). Associated items let you enforce this property in the type system. Because of this we can have sane iterator type inference, especially when used with for loops and when using iterator combinators like collect or map.

5 Likes

The way I understand it is as if & then. It’s better to explain on std::ops::Add:

impl std::ops::Add<&str> for String { // *If* you pick this implementation,
    type Output = String // *then* it'll use this type.

So you can choose to add String + &str, and then this implementation will say you’re getting String back (and you can’t say it’ll return i32).

As defined, there can only be one implementation of std::ops::Add<&str> for String, and as it happens, that one implementation has the Output type of String.

Whereas if the trait were std::ops::Add<Input, Output>, there could in theory be multiple impls of std::opts::Add<&str, ...> for String with different Output types, and the compiler would constantly be asking you which one you meant, getting back to the “sane inference” point.

That’s a good way to think of associated types.

Implication is also deeply connected to function abstraction (it’s not an accident that function types are written A -> B which looks like an implication).

Thus, we can think of associated types as type level functions where <Arg as Fun>::Result denotes function application.

1 Like