C.W.K.
Stream
Lesson 06 of 07 · published

Associated Types

~11 min · traits, associated-types, iterator

Level 0Rust Curious
0 XP0/80 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete

Some traits need to refer to a type that the implementor chooses. An associated type is a placeholder declared in the trait and pinned down by each implementation. The canonical example is Iterator.

The Iterator trait

Iterator declares type Item; and fn next(&mut self) -> Option<Self::Item>. Each implementor picks what Item is — a counter yields u32, a line reader yields String. Self::Item in the signature refers to whatever that implementation chose.

Associated type vs generic parameter

You could imagine Iterator<Item> as a generic instead. The difference: a generic parameter lets a type implement the trait many times with different parameters; an associated type pins one choice per implementation. Since a given iterator yields exactly one element type, an associated type is the right fit — cleaner signatures, no ambiguity at call sites.

Associated type = one choice per impl; generic param = many impls. Use an associated type when each implementor has exactly one sensible choice (an iterator's element type). Use a generic parameter when a type might implement the trait for several different types — like From<T>, implemented many times for many source types.

Implementing Iterator

Implement next and pick Item, and your type instantly gains the whole iterator toolkit — map, filter, sum, collect — all as default methods built on your next. That's the require-little-provide-lot design from the defaults lesson, paying off in the standard library's most-used trait.

Code

One next(), a whole iterator for free·rust
struct Counter { count: u32 }

impl Iterator for Counter {
    type Item = u32;                  // associated type: this iterator yields u32
    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 3 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let c = Counter { count: 0 };
    // implementing next() unlocks map/filter/sum for free:
    let total: u32 = c.map(|x| x * 10).sum();
    println!("{total}"); // 60
}

External links

Exercise

Implement Iterator for a Fibonacci struct that yields u64 values. Pick the associated Item type, implement next, then call .take(10).collect::<Vec<_>>() on it. Where did take and collect come from if you only wrote next?
Hint
take and collect are default methods on Iterator, built atop the next you implemented. Choosing type Item = u64 is what makes the whole toolkit yield u64s.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.