Hedronite Lesson · Polyglot-Dev / Rust · Tue 2026-09-22

Iterators

Lazy until consumed. Adapt with map and filter. Finish with collect.

Lesson Class: Duha (Rust language track)
Focus: lazy iterators · next · iter/into_iter/iter_mut · map · filter · collect · sum
Code Blocks: clean blocks, explanation in prose
Done-criteria: map+collect chain + filter that captures
Grounding: TRPL stable Ch.13 HTML ch13-00 + ch13-02 · ch13-03/04 deferred · Blandy not cited
The laziness
iter() creates a walker; nothing runs until consumption.
The motor
next returns Some until None; mut tracks position.
The chain
map and filter adapt; collect or sum finishes the walk.
Lazy until consumed. Adapt with map and filter. Finish with collect.

<!-- hal:authoritative:yaml -->

*Walk a sequence without hand-rolled indexes. Create an iterator with no effect until something consumes it. Adapt with map and filter, then finish with collect or another consuming method.*

§I — Frame

Duha session 15. TRPL Chapter 13 continues after closures (session 14). Today is iterators only. Session 14 left you with capture, move, and FnOnce / FnMut / Fn. Keep those closures; you will pass them into adapter methods here. Do not rewrite minigrep. Do not open ch13-03 or ch13-04.

Three moves land by the end:

  1. Create without effect: iter() stores a lazy walker; nothing runs until consumption.
  2. Consume or adapt: sum eats the iterator; map / filter return new lazy iterators.
  3. **Capture in filter:** a closure that closes over an outside value (shoe_size) and keep matching items.

Syllabus Done-criteria: Can write a map+collect chain and a filter that captures an outside value.

§II — Lazy until consumed; next drives the walk

An iterator owns the logic of "what comes next" and "when we are done." You do not restart at index zero and bump a counter yourself.

Iterators in Rust are lazy. Calling iter() on a Vec creates an iterator and stores it. Alone, that call does nothing useful:

let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();

A for loop consumes the iterator. Under the hood it takes ownership of v1_iter and pulls values until None:

for val in v1_iter {
    println!("Got: {val}");
}

Without iterators you would start an index at zero, index into the vector, and bump until length. Iterators own that bookkeeping so the same walk works for sequences you cannot index into. The for form hides mutability: the loop takes the iterator and mutates it behind the scenes.

The Iterator trait requires an associated type Item and one method:

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

next returns Some(item) until the sequence ends, then None. Manual calls need mut because each next advances internal state:

let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);

iter() yields immutable references. into_iter() takes ownership and yields owned values. iter_mut() yields mutable references. Pick the method that matches how you need to touch each element.

§III — Consuming adapters vs iterator adapters

Methods that call next until the end are consuming adapters. sum takes ownership of the iterator, adds every item, and returns the total. After sum, the iterator name is gone:

let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum();
assert_eq!(total, 6);

Iterator adapters do not consume. They return a new iterator that changes how items appear. map takes a closure and yields transformed items. Alone, it is still lazy — the closure never runs, and the compiler warns about an unused Map:

v1.iter().map(|x| x + 1); // unused Map; nothing happens

collect consumes the adapted iterator into a collection. That is the usual fix:

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);

Chain adapters as needed. Laziness means the chain does no work until a consuming method (collect, sum, for, and others) pulls values through. Closures customize each step while the Iterator trait reuses the walk. That is why session 14 sits under this chapter: adapters take the same capture rules you already practiced.

§IV — filter that captures

filter keeps items when its closure returns true. Closures from session 14 fit here: the predicate often captures a value from outside.

TRPL's shoe example owns a Vec<Shoe>, takes a target size, and returns only matching shoes:

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter()
        .filter(|s| s.size == shoe_size)
        .collect()
}

into_iter steals the vector. The closure captures shoe_size by immutable borrow and compares each shoe. collect builds the kept Vec. That is the Done-criteria pattern: adapter + capture + consume.

§V — One complete proof

  1. Create v1.iter() and say why that line alone has no side effect.
  2. Call next on a mut iterator until None; name why mut is required.
  3. Write v1.iter().map(|x| x + 1).collect::<Vec<_>>() (or Vec<_> annotation) and assert the result.
  4. Explain the unused-Map warning when collect is omitted.
  5. Write a filter whose closure captures an outside value (size, threshold, or similar) and collect the kept items.

When those five hold, session 15's selected depth is done. Ch.13 I/O rewrite and performance wait for later sessions.

§VI — Closing

Iterators walk a sequence without hand indexes. Creation is cheap and lazy; next is the motor; consuming adapters finish the walk; adapter methods reshape the stream until something consumes it. Closures from session 14 become the bodies of map and filter. Next TRPL depth after this split follows the syllabus when a teammate seats it.

Done-criteria: Can write a map+collect chain and a filter that captures an outside value.

Related