Closures & Iterators
Closures come in three flavors — Fn, FnMut, FnOnce — and iterator chains stay lazy until consumed, yet compile as fast as a hand-written loop.
A closure is Rust's anonymous function — but unlike a plain fn, a closure can capture variables from the scope where it's defined and carry them along wherever it goes. The syntax leans on inference: |x| x + 1 needs no type annotations because the compiler works out x's type (and the return type) from how the closure is first used, the same way it infers the type of a let binding. This makes closures ideal for the two things you'll use them for constantly in Rust: passing a small piece of logic into another function (a comparator, a callback, a predicate) and building the pipelines that Iterator methods run on.
That inference comes with one wrinkle worth knowing up front: once a closure's parameter and return types are inferred from its first call, they're locked in — you can't call the same closure with an i32 the first time and an f64 the second, the way a generic function could. If you need that kind of flexibility you want a generic function with a trait bound instead; a closure's types, once inferred, are as concrete as if you'd written them by hand.
How a closure captures its environment determines which of three traits it implements, and Rust chooses the least restrictive one it can get away with based on what the closure's body actually does. Every closure implements FnOnce, because every closure can be called at least once. If the body doesn't move a captured value out (it only reads or mutates through a reference), the closure also implements FnMut, meaning it can be called repeatedly, mutating its captures along the way. If the body doesn't need to mutate anything either — it only reads — the closure additionally implements Fn, and can be called any number of times, even concurrently from multiple places. These aren't three unrelated traits you pick between; they form a hierarchy, and a function parameter bounded by Fn will happily accept closures that also implement FnMut and FnOnce, but a parameter bounded by FnOnce will accept only ones that are at most that permissive.
By default, a closure captures the minimum it needs, and by reference when it can get away with it — which is efficient but occasionally not what you want, especially once threads get involved. The move keyword forces a closure to take ownership of everything it captures, even variables it would otherwise have only borrowed. This matters most with thread::spawn, whose closure might run after the current function has already returned, so it can't be allowed to hold a borrow into a stack frame that may no longer exist — move is what makes handing data to a new thread sound.
Iterator is a trait, and a surprisingly small one: implementing it requires only a single method, fn next(&mut self) -> Option<Self::Item>, which returns Some(item) until the sequence is exhausted and None after. Everything else — map, filter, fold, collect, and dozens more — is provided as default methods built on top of next, the same default-method mechanism from the traits lesson, just used at a much larger scale. Crucially, iterators are lazy: calling .map(...) on an iterator doesn't loop over anything or compute a single value, it just wraps the original iterator in a new one that will apply the closure when — and only when — something eventually asks it for its next item.
That laziness is what makes iterator chains compose so cleanly. numbers.iter().map(|n| n * n).filter(|n| n % 2 == 0).fold(0, |acc, n| acc + n) reads as a pipeline — square, then keep the even ones, then sum — and nothing actually runs until fold starts pulling items through the chain one at a time, doing all three steps to one element before moving to the next, rather than materializing an intermediate vector after each stage. collect is the adaptor you'll reach for most often to end a chain, because it can build practically any collection the compiler can infer from context — a Vec, a HashMap, a String — just by annotating the binding's type.
It's reasonable to expect all of this abstraction to cost something at runtime — extra function calls, wrapper structs, indirection — but it largely doesn't. This is Rust's 'zero-cost abstractions' principle in action: because every adaptor's type is known at compile time and every closure is a concrete, inlinable type (not a boxed dynamic value unless you ask for one), the optimizer can see straight through the entire chain and generate code that looks, after optimization, essentially identical to the hand-written for loop with an if and a running total that you'd have written instead. You get to write the readable, declarative version and still get the assembly of the imperative one.
fn apply_to_five<F>(f: F) -> i32whereF: Fn(i32) -> i32,{f(5)}fn main() {let offset = 3;// This closure only reads `offset`, so it implements `Fn`// (and therefore also `FnMut` and `FnOnce`).let add_offset = |x| x + offset;println!("{}", apply_to_five(add_offset));// The same closure can be called again: it never consumed `offset`.println!("{}", add_offset(10));}
A closure that only reads its captured variable implements Fn, so it can be passed into a generic function and then called again directly afterward.
use std::thread;fn main() {let data = vec![1, 2, 3];// `move` forces the closure to take ownership of `data` instead of// borrowing it, which is required here: the spawned thread might// outlive the current scope, so it can't hold a borrow into it.let handle = thread::spawn(move || {println!("data from thread: {:?}", data);});handle.join().unwrap();}
The move keyword forces the closure passed to thread::spawn to take ownership of data, which is required because the spawned thread could outlive the current stack frame.
fn main() {let numbers = vec![1, 2, 3, 4, 5, 6];// Nothing runs yet: `map` and `filter` just build up a lazy pipeline.let pipeline = numbers.iter().map(|n| n * n).filter(|n| n % 2 == 0);// The pipeline only executes once something consumes it, like `fold`.let sum_of_even_squares = pipeline.fold(0, |acc, n| acc + n);println!("sum of even squares: {}", sum_of_even_squares);// `collect` is the adaptor you'll reach for most often to end a chain.let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();println!("{:?}", doubled);}
An iterator pipeline that does nothing until fold consumes it, illustrating both laziness and the map/filter/fold/collect adaptors.
🧠 Check your understanding
0/1 · 0/1 answered1. You want to pass a closure to a function that calls it repeatedly in a loop. The closure captures a Vec<i32> from its environment and calls .push() on it each time it runs. Which trait bound must the function require, and why?