Iterators & Closures
Yeah yeah, I’ll do it later
Iterators in Rust are lazy. That means when you write something like
(0..1_000_000).map(|n| n * 2), exactly zero multiplications happen. No work
gets done. You’ve just described the work. The actual computation only kicks
off when something asks for values — a for loop, a .collect(), a .sum(),
or any of the other “consumer” methods.
This sounds like a small detail. It’s the entire reason iterator chains are fast. The compiler sees the whole pipeline at once and welds it into a single tight loop — no intermediate vectors, no allocations between steps. The resulting machine code is essentially identical to what you’d hand-write in C. This is called zero-cost abstraction and it’s one of Rust’s bragging rights.
The shape of every iterator chain is:
- A source —
(1..=10)here. Could also bevec.iter(),string.chars(),hashmap.values(), etc. - Zero or more adapters —
map,filter,take,skip,enumerate,zip. Each adapter wraps the previous iterator and produces a new one. - A consumer —
collect,sum,count,forloop,find,fold. This is what kicks the chain into motion.
Closures — Rust’s lambdas
A closure is an anonymous function. The syntax is two vertical bars holding the parameters, then the body:
|x| x + 1|a, b| a * b|s: &str| s.len()|| println!("no args!")Closures can capture variables from their surroundings, hence the name.
Rust has three “flavours” of closure, distinguished by how they capture their environment:
Fn— borrows immutably. Can be called any number of times.FnMut— borrows mutably. Can be called any number of times, but only one caller at a time. Useful for closures that update some state each time they run.FnOnce— takes ownership of captured values. Can be called once.
You almost never have to write these traits out by hand — the compiler picks the right one based on what your closure body does. You’ll see them in function signatures, though, when you start writing functions that take closures as arguments.
Some higher-orderism is in order
A few of the iterator adapters you’ll use constantly:
map
Transforms each element.
filter
Keeps elements that match a predicate.
enumerate
Pairs each element with its index. Saves you a manual counter.
zip
Pairs elements from two iterators together. Stops at the shorter one.
take and skip
Slice off the beginning or end of an iterator.
That (1..) is an infinite range. take(5) is what makes it terminate.
Iterators can be infinite — laziness saves the day.
Folds — reducing many things to one thing
When you want to reduce an iterator down to a single value, you use a fold
(also known as reduce, accumulate, or — in the iterator world — fold).
fold takes a starting value and a closure of (accumulator, item) -> new accumulator. For sums and products specifically, the standard library has
.sum() and .product() which are clearer:
Iterators on the things you already have
Almost everything in the standard library hands you an iterator if you ask.
| Type | What you call | What you get back |
|---|---|---|
Vec<T>, &[T] | .iter() | iterator of &T |
Vec<T> | .iter_mut() | iterator of &mut T |
Vec<T> | .into_iter() | iterator of T (consumes the vec) |
&str | .chars() | iterator of char |
&str | .bytes() | iterator of u8 |
&str | .split(' ') | iterator of &str |
HashMap<K, V> | .iter() | iterator of (&K, &V) |
HashMap<K, V> | .values() | iterator of &V |
Range<i32> | (already one) | iterator of i32 |
A for loop in Rust is actually sugar over .into_iter():
for x in xs { … }// is roughlylet mut it = xs.into_iter();while let Some(x) = it.next() { … }That’s why for x in &xs borrows ((&xs).into_iter() yields &Ts) and for x in xs
consumes (yields Ts and you can’t use xs afterwards).
A worked example — find the longest word
max_by_key and min_by_key are little iterator gems. They take a closure
that extracts a comparable key from each element and return whichever element
maximises (or minimises) that key.
Building your own iterator
You can implement Iterator for your own types. You write one method — next
— and you get the entire ecosystem of adapters and consumers for free.
Notice we used .take(10) on our own custom iterator. That’s the payoff:
implement one method, get a hundred adapters for free.
The recap
- Iterators are lazy. Nothing happens until a consumer asks.
map,filter,enumerate,zip,take,skipare the everyday adapters;collect,sum,fold,forare the everyday consumers.- Closures come in
Fn,FnMut,FnOnceflavours; the compiler picks the right one for you. .iter()borrows,.into_iter()consumes,.iter_mut()borrows mutably.- You can implement
Iteratorfor your own types by writing a singlenextmethod.
Next: Vec, HashMap, and the rest of the standard collections.