Iterators & Closures
Laziness, Fn / FnMut / FnOnce, common adaptors.
Iterators & Closures
Rust's iterators are the most powerful general-purpose abstraction in the language. They are lazy, zero-cost, and fused — a chain of map, filter, fold compiles to the same machine code as a hand-written loop, with the side benefit of being readable.
Closures provide the lambdas that iterators consume, and they come with three traits — FnOnce, FnMut, Fn — that classify them by how they capture and how often they can be called.
The Iterator trait
A single trait with a single required method:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ...dozens of provided methods
}
Implement next, and you get map, filter, take, skip, chain, zip, fold, sum, count, min, max, collect, etc. for free. Any type that pulls one item at a time can become an iterator.
The standard ways to get an iterator from a collection:
let v = vec![1, 2, 3];
v.iter() // Iterator<Item = &i32> — borrows
v.iter_mut() // Iterator<Item = &mut i32> — mutable borrow
v.into_iter() // Iterator<Item = i32> — consumes the vec
for x in v is sugar for v.into_iter(). for x in &v is sugar for v.iter(). for x in &mut v is v.iter_mut().
Adapters vs consumers
Two kinds of methods:
- Adapters return another iterator. They do nothing on their own.
map,filter,take,skip,chain,zip,enumerate,flatten,flat_map,filter_map,inspect,peekable... - Consumers drive the iterator forward and produce a value.
collect,sum,product,count,fold,for_each,find,any,all,min,max,last,nth,reduce...
This compiles, runs nothing, and emits a warning:
(1..10).map(|x| x * 2).filter(|&x| x > 5); // ⚠ unused, no consumer
This actually does the work:
let sum: i32 = (1..10).map(|x| x * 2).filter(|&x| x > 5).sum();
The chain is described declaratively; the consumer pulls values through it on demand.
collect — the universal consumer
collect materialises an iterator into any collection that implements FromIterator:
let v: Vec<i32> = (0..5).collect();
let s: String = ['h', 'i'].iter().collect();
let m: HashMap<&str, i32> = [("a", 1), ("b", 2)].iter().copied().collect();
let set: HashSet<i32> = vec![1, 1, 2, 3].into_iter().collect();
When the target type is ambiguous, use the turbofish syntax to spell it out:
let v = (0..5).collect::<Vec<i32>>();
let v: Vec<_> = (0..5).collect(); // or annotate the binding
Special trick: collect::<Result<Vec<T>, E>>() short-circuits on the first Err:
let parsed: Result<Vec<i32>, _> = vec!["1", "2", "abc"].iter()
.map(|s| s.parse::<i32>())
.collect();
// parsed is Err(...) — bails on "abc"
fold — the swiss army knife
let sum = (1..=10).fold(0, |acc, x| acc + x);
let product = (1..=5).fold(1, |acc, x| acc * x);
let joined = ["a", "b", "c"].iter()
.fold(String::new(), |mut s, x| { s.push_str(x); s });
fold(init, |acc, x| new_acc) carries an accumulator through every item. Most other consumers are special-case folds: sum is fold(zero, |a, x| a + x); count is fold(0, |a, _| a + 1).
reduce(|a, b| ...) is fold without an explicit init — it uses the first element as the seed, returning Option<T> (None if the iterator is empty).
The closure traits
Closures are anonymous functions that capture free variables from their environment:
let factor = 10;
let multiply = |x: i32| x * factor; // captures `factor`
multiply(5); // 50
The compiler classifies each closure into one of three traits, based on what it captures and how:
| Trait | Captures by | Can be called | Example |
|---|---|---|---|
Fn |
Shared reference (&T) |
Many times, even concurrently | |x| x + factor (read-only) |
FnMut |
Exclusive reference (&mut T) |
Many times, but one at a time | |x| { count += 1; x } |
FnOnce |
By value (T) |
Once — captures are consumed | || { drop(s) } |
Fn ⊂ FnMut ⊂ FnOnce. A closure that implements Fn also implements FnMut and FnOnce. The compiler always picks the most permissive (Fn if possible, falling back to FnMut, finally FnOnce).
You usually don't write these traits explicitly. They appear as bounds when you write higher-order functions:
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 { f(x) }
fn apply_once<F: FnOnce() -> String>(f: F) -> String { f() }
fn each<F: FnMut(i32)>(xs: &[i32], mut f: F) { for &x in xs { f(x) } }
move keyword — force capture by value
By default, closures capture in the most permissive way that works. move forces by-value capture:
let s = String::from("hello");
let owned = move || println!("{}", s); // closure now owns s
// println!("{}", s); // ❌ s was moved into the closure
move is essential for closures that escape their defining scope — most commonly when spawning a thread:
let v = vec![1, 2, 3];
std::thread::spawn(move || {
println!("{:?}", v); // v lives in the new thread
});
Without move, the new thread might outlive v, which would dangle. The compiler refuses; move is the fix.
Common adapters worth memorising
// Mapping
(1..=3).map(|x| x * 2); // 2, 4, 6
(1..=3).map(|x| x.to_string()); // String items
// Filtering
(1..=10).filter(|&x| x % 2 == 0); // 2, 4, 6, 8, 10
xs.iter().filter_map(|s| s.parse::<i32>().ok()); // skip non-numeric
// Pairing / windowing
xs.iter().enumerate(); // (0, &x0), (1, &x1), ...
xs.iter().zip(ys.iter()); // (&x0, &y0), ...
(1..).take(5); // first 5 of an infinite range
// Flattening
vec![vec![1, 2], vec![3, 4]].into_iter().flatten(); // 1, 2, 3, 4
xs.iter().flat_map(|s| s.chars()); // chars across all strings
// Side-effects (rarely a good idea — use for_each at the consumer end)
xs.iter().inspect(|x| println!("saw {x}")).count();
Consumers worth memorising
let xs = vec![1, 2, 3, 4, 5];
xs.iter().sum::<i32>(); // 15
xs.iter().product::<i32>(); // 120
xs.iter().count(); // 5
xs.iter().min(); // Some(&1)
xs.iter().max(); // Some(&5)
xs.iter().any(|&x| x > 3); // true (short-circuits)
xs.iter().all(|&x| x > 0); // true
xs.iter().find(|&&x| x > 2); // Some(&3)
xs.iter().position(|&x| x == 4); // Some(3)
xs.iter().for_each(|x| println!("{x}")); // side-effect consumer
Many of these short-circuit — any stops at the first true, find stops at the first match, take_while stops at the first false. Lazy iterators make this efficient by construction.
Closures as return types — impl Trait
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add5 = make_adder(5);
add5(3); // 8
impl Fn(...) -> ... says "some specific anonymous type that implements Fn". Each closure has a unique compiler-generated type — impl Trait lets you return them without naming.
For storing different closures in the same place (e.g. a Vec), use a trait object:
let actions: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|x| x + 1),
Box::new(|x| x * 2),
];
Box<dyn Fn> is dynamic dispatch through a vtable — slightly slower, more flexible.
Why this all matters
The for loop you'd write in any other language is in Rust competing with a chain of iterator methods that:
- compose without intermediate allocations,
- describe intent clearly without manual indexing,
- benefit from the same compiler optimisations as the loop,
- and integrate with
rayonso.iter()becomes.par_iter()for free parallelism.
It takes a few weeks for the patterns to feel natural. Once they do, almost every imperative loop in your code rewrites into something shorter and harder to mis-bound.
Tools in the wild
4 tools- libraryitertoolsfree tier
Extra iterator adaptors: `chunks`, `tuples`, `unique`, `group_by`, `cartesian_product`.
- libraryrayonfree tier
Parallel iterators — swap `.iter()` for `.par_iter()` and the work is split across threads.
- cliclippyfree tier
Suggests iterator chains over manual loops, flags `collect()` -> `for` patterns.
- clicargo-asmfree tier
Inspect the generated assembly to confirm iterator chains compile to tight loops.