Last lesson ended with a promise: a tool that calls closures over and over, once per element of a collection, chained into a single readable sentence. Here it is — and it's been hiding inside every for loop you've written since the control-flow lesson.
The loop you already know
An ordinary for loop: walk a list of numbers, keep the even ones, double them.
The same thing, as a chain
Same vector, same result, written a second way:
Four pieces, read left to right as one sentence:
.into_iter()turnsnumbersinto an iterator — something that hands out its elements one at a time. (Lesson 13 mentioned.into_iter()in passing as an example of a method that takes plainself; now you know exactly why — it consumes theVecto produce something that owns its values.).filter(|&n| n % 2 == 0)keeps only the items where the closure returnstrue.filter's closure receives a reference to each item —&n, the same destructuring-reference pattern from last lesson'ssort_by_key, undoing exactly one layer of&to get a plaini32..map(|n| n * 2)transforms every item that survived the filter.map's closure receives the value itself — no&— becausemapis allowed to consume each item to produce something new..collect()gathers everything into aVec<i32>— the type annotation ondoubled_evensis what tells.collect()what shape to build.
And here's the headline: for n in numbers { ... } is, underneath, exactly numbers.into_iter() plus a loop that keeps calling .next() until there's nothing left. You've been using iterators since the control-flow lesson — every for loop you have ever written in this course was one — exactly the same shape as generics since lesson 14, and |x| expression since lesson 16: the idea was always there and working, just without its name.
What an iterator actually is
Time to look at the name directly. Here is the entire Iterator trait — slightly simplified, but capturing the idea exactly:
type Item is a placeholder — the same idea as <T> from the generics lesson, spelled differently: "this trait has a type that depends on what's iterating — fill it in." For numbers.into_iter(), Item is i32. And next: one method, returning Option<Self::Item> — Some(item) for as long as there's something left, None the moment there isn't. That's it. That is the entire thing a type has to implement to become an iterator. Everything else — .filter(), .map(), .collect(), .sum(), dozens more — is a default implementation, the same impl Summary for NewsArticle {} idea from the traits lesson, except here the "empty impl" is doing almost all of the work, every time, for every iterator that has ever existed.
Watching next() directly
You can call next() yourself, by hand, to see exactly what a for loop does on your behalf:
Four calls, four lines: Some(34), Some(50), Some(25), then None. Three numbers, three Somes, and one final None announcing there's nothing left — Option<T>, from lesson 14, doing exactly the job it has always done, here used by the standard library on itself. iter needs to be mut for a reason that should feel familiar: every call to .next() changes iter — it has to remember where it left off — so calling it requires a mutable reference to self, the same &mut self from the method-syntax lesson.
The index problem: .enumerate()
Back in the control-flow lesson, a "Rust doesn't have C-style for loops" aside left you with a name and a promise: if you ever want an index, look at .enumerate() — "we'll meet it properly in the iterators lesson." The slice-type lesson already put it to work, silently, in for (i, &item) in bytes.iter().enumerate() — two pieces of unexplained syntax sitting in code that worked. Both get explained now, with the same vector this lesson started with:
.enumerate() is one more adaptor — same family as .filter() and .map(), wrapping an iterator without running anything by itself. What it changes is what .next() hands back: instead of Some(value), every call now returns Some((index, value)) — a tuple pairing a usize position with the item itself. for (index, value) in ... destructures that tuple directly in the loop's pattern, two names instead of one.
And .iter(), not .into_iter() — on purpose. .into_iter() consumes numbers; here, looking at each value is enough, so .iter() borrows instead, the same call the references-and-borrowing lesson introduced. That's why value is &i32, not i32 — and why the slice-type lesson's pattern was (i, &item), not (i, item): that extra & undoes the reference right there in the pattern, the exact same move as |&n| from .filter() earlier in this lesson, just spelled inside a tuple instead of a closure argument. ({value} still prints 34, not an address — references print exactly like the values they point to.)
Adaptors vs. consuming methods: .sum()
.filter() and .map() are called adaptors — they describe a change to a pipeline without running anything. Nothing loops, nothing calls a closure, until something consumes the iterator. .collect() is one consuming method. Here's another:
Same .filter() as before — keep 34, 50, and 100 — but instead of .map() and .collect(), .sum() consumes the whole pipeline directly and hands back one number: 184. Swap .sum() for .count() and you'd get 3 — how many evens there are — instead of their total. Neither .filter() nor the closure inside it ran even once until .sum() showed up and asked for a result; an iterator chain, on its own, is just a description of work, not the work itself. That's why chains can be as long as you like without worrying about wasted intermediate Vecs — the whole pipeline runs as one pass over the data, item by item, only when something finally asks.
Quick exercise
- Take the
numbersandcharsvectors from the generics lesson —vec![34, 50, 25, 100, 65]andvec!['y', 'm', 'a', 'q']. For each, comparelargest(&the_vec)tothe_vec.iter().max(). Same answer, wrapped differently —&100versusSome(&100)..max()is.sum()'s sibling: one more consuming method, handing back the largest item the iterator produced, wrapped inOption— because an empty iterator genuinely has no maximum, the same honestNoneyou just watched.next()return. - Write a chain that takes
vec!["apple", "fig", "watermelon", "kiwi"]and produces aVec<usize>of the LENGTHS of words longer than 4 characters —.into_iter(),.filter(),.map(),.collect(), in that order. (Hint:.map()here turns a&strinto ausizevia.len()— a method call, so there's no&to undo.)
Add up what just happened. for loops, since the control-flow lesson — iterators. |x| expression, since lesson 16 — closures, now fully explained. Option<T>, since lesson 14 — the return type of .next() itself, quite possibly the most-called function in the entire standard library. Generics, since lesson 23 — type Item, the same placeholder idea wearing different syntax. Default trait implementations, from the traits lesson — the entire reason .filter(), .map(), .collect(), .sum(), .max(), and .enumerate() exist at all, on top of one method — .enumerate() in particular closing a loop the control-flow lesson opened back at the start of this course. Almost nothing in this lesson was new. All of it was a name, finally attached to something you'd been doing since the first time you wrote for x in &my_vec.
And here's the same "genuinely surprising part" from the generics lesson, back for an encore: numbers.into_iter().filter(|&n| n % 2 == 0).map(|n| n * 2).collect() compiles down to the same machine code as the hand-written for loop at the top of this lesson — no hidden cost, no extra allocation, nothing. Monomorphization again, just applied to iterator adaptors instead of generic functions. And last lesson's three closure traits finally meet their real signatures: .filter() and .map() both require FnMut — callable repeatedly, once per element. Every Fn closure is automatically a FnMut closure too, which is exactly why |&n| n % 2 == 0 and |n| n * 2 — both pure, read-only, Fn closures — slot in without any fuss.
One more thing before you go. Every program in this course — closures, iterators, generics, traits, all of it — has lived in a single file. That stops being realistic the moment a project grows past a few hundred lines. The next lesson is about organizing code: modules, the pub keyword that's been sitting in front of plenty of functions and structs along the way without a full account of what exactly it opts out of, and how packages and crates fit together — the last piece of vocabulary between you and building something real: modules and crates.