Differece between type Item and <Item>

iter

Hello!

What's the difference/advantage/disadvantage between using one over the other? In the video, it was said that I'd prefer to use the associated type if I expect there to be only one implementation of the trait but I don't know what that means in this context.

With the first trait, each type can only implement Iterator at most once, but with the second trait, a type may implement both Iterator<i32> and Iterator<String> at the same time.

Let's take str: one can imagine it being potentially an iterator of chars as well as an iterator of bytes (u8s):

impl Iterator<u8> for &'_ str {
    fn next (self: &'_ mut Self) -> Option<u8>
    { … }
}
impl Iterator<char> for &'_ str {
    fn next (self: &'_ mut Self) -> Option<char>
    { … }
}

This would indeed be only possible if the trait Iterator had been defined with a generic Item parameter rather than an associated type.

But since it uses an associated type, if you tried to write such impls, you'd have a "conflicting impls" error:

impl Iterator for Thingy {
    type Item = u8;
    …
}
impl Iterator for Thingy {
    type Item = char;
    …
}

type WhichIsIt = <Thingy as Iterator>::Item; // ???

Thanks, I see. But is there advantage in terms of performance of limiting implementation to one?

There shouldn't be:

  • all this happens at compile-time, so it will have no impact whatsoever at runtime (c.f. "zero cost abstractions")

  • I'd expect the compile-time difference to be negligible, if any.

Where it matters is w.r.t. type inference. If we imagine that world where Iterator would have been generic over Item, then for c in "some_string" { println!("{}", c) } would lead to a type inference error because it wouldn't know whether item would be a u8 or a char.

  • A nice example to compare the two approaches is AsRef<T> vs. Deref<Target = T>. Indeed, try and see what happens when you do

    use ::core::convert::AsRef;
    println!("{}", String::from("…").as_ref());
    

    vs.

    use ::core::ops::Deref;
    println!("{}", String::from("…").deref());
    

With an associated type we don't have that problem :slightly_smiling_face:

  • Granted, sometimes Rust is smart enough to observe that there is only one generic parameter for which a generic trait is implemented for a type, and in that case it may be able to resolve the type inference issue on its own. But this is fragile, since anybody can bring their own impls to the table, breaking this "the only one by default" behavior / reasoning. It also makes the whole documentation and API less clear, for the cases where you truly intend to only allow one implementor. This has been the rationale behind Iterator, for instance.

Aside: for the case of &str, Rust has still managed to support both shapes in an unambiguous manner by not making &str be an iterator / iterable per se, but rather, by making it yield two different iterators through the special .bytes() and .chars() methods. So, as you can see, using associated types is not even that restrictive :wink:

2 Likes

Well, the difference is the same as the difference between function arguments and return values. Think of traits and generic types as "type-level functions". That's what they are.

In Trait<T>, the type parameter T is really a type variable that the user of the trait choses. You can say you want a type to implement Trait<bool> or Trait<String> or whatever. Many generic types impl Trait<T> for all choices of T, for example.

However, in the trait Iterator, the Item is an associated type. An associated type is something that the implementation of the trait choses, and the user of the trait has no influence on it. Therefore you can't request, for example, an Iterator<Item=bool> from "some string".chars(). The iterator returned by str::chars() simply doesn't and can't return an iterator over booleans; its implementation defines that it will yield char.


One interesting detail is that since a type system is all about constraints, you can still apply bounds and equalities to an associated type in a where clause, in which case the constraint sort of "propagates" backwards to the implementing type. E.g. the following function will only accept iterators that yield char:

fn consume_chars<I>(iter: I)
    where
        I: Iterator<Item=char>,
{
    let _: Vec<char> = iter.collect();
}
3 Likes

RFC 195 gives some more examples of inference problems solved by associated types, as well as some other advantages.

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.