Skip to content

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.

🦀 main.rs

The shape of every iterator chain is:

  1. A source(1..=10) here. Could also be vec.iter(), string.chars(), hashmap.values(), etc.
  2. Zero or more adaptersmap, filter, take, skip, enumerate, zip. Each adapter wraps the previous iterator and produces a new one.
  3. A consumercollect, sum, count, for loop, 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.

🦀 main.rs

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.

🦀 main.rs

Some higher-orderism is in order

A few of the iterator adapters you’ll use constantly:

map

Transforms each element.

🦀 main.rs

filter

Keeps elements that match a predicate.

🦀 main.rs

enumerate

Pairs each element with its index. Saves you a manual counter.

🦀 main.rs

zip

Pairs elements from two iterators together. Stops at the shorter one.

🦀 main.rs

take and skip

Slice off the beginning or end of an iterator.

🦀 main.rs

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).

🦀 main.rs

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:

🦀 main.rs

Iterators on the things you already have

Almost everything in the standard library hands you an iterator if you ask.

TypeWhat you callWhat 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 roughly
let 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

🦀 main.rs

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.

🦀 main.rs

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, skip are the everyday adapters; collect, sum, fold, for are the everyday consumers.
  • Closures come in Fn, FnMut, FnOnce flavours; the compiler picks the right one for you.
  • .iter() borrows, .into_iter() consumes, .iter_mut() borrows mutably.
  • You can implement Iterator for your own types by writing a single next method.

Next: Vec, HashMap, and the rest of the standard collections.