Closures and iterators — abstraction that compiles away
Lesson 13 abstracted over types; much of what a program passes around is behavior — a sort key, a filter, a thread's body — that needs some of the caller's variables. When every variable has an owner, "this code uses x" must say how: as a reader, a writer, or the new owner. A closure is exactly that: a struct of the variables it uses, each held in one of the three ownership modes, whose traits Fn, FnMut, FnOnce are the three receivers. Iterators apply the same modes to a sequence, and in the build measured here a chain of adaptors and closures compiled to the hand-written loop.
New idea: a closure is an anonymous struct of the places its body uses, each held in one of the three ownership modes — shared borrow, exclusive borrow, by value; its trait is the receiver its body needs, and iterators apply the same modes to a sequence.
Forces next: Closures showed the three ownership modes at work: move, shared, exclusive. But everything so far has exactly one owner and a borrow law checked before the program runs. What about data that genuinely has several owners, or must change through a shared handle?
1 · Behavior with a memory
Sorting people by age takes behavior as an argument; so do counting those above a threshold and totalling their ages:
struct Person { name: String, age: u32 }
fn main() {
let mut people = vec![
Person { name: String::from("Ada"), age: 36 },
Person { name: String::from("Linus"), age: 21 },
Person { name: String::from("Grace"), age: 45 },
];
people.sort_by_key(|p| p.age); // uses only its argument
let min_age = 30;
let older = people.iter().filter(|p| p.age >= min_age).count(); // and a local
let mut total = 0;
people.iter().for_each(|p| total += p.age); // changes a local
println!("{} {older} {total}", people[0].name);
}
Linus 2 102
Each |p| … is a closure, a function written in place. The first uses only its argument; the filter also reads min_age, and the for_each changes total. A function pointer is only a code address; these need the code together with the variables it uses (compare the Scala track). Ownership allows three ways to use a variable you do not own — read it through a shared borrow, change it through an exclusive borrow, take it by a move — so a closure holds each variable in one of those modes, the weakest its uses allow.
The hazard is the C++ track's stored [&] lambda that outlives its local: Lesson 01's dangling return, with a closure as the stale name. Drop the mut from total and the chosen mode shows:
fn main() {
let ages = vec![36, 21, 45];
let total = 0;
ages.iter().for_each(|a| total += a); // the closure must change total
println!("{total}");
}
error[E0596]: cannot borrow `total` as mutable, as it is not declared as mutable
4 | ages.iter().for_each(|a| total += a); // the closure must change total
| ^^^^^ cannot borrow as mutable
Nobody wrote &mut total, yet rustc "cannot borrow as mutable": the capture is the borrow, refused as Lesson 05 refuses it anywhere. So what kind of name is a closure?
2 · A closure is an anonymous struct
The Reference describes a closure as a value of a unique, anonymous type, roughly a struct of its captured values, that implements the call traits. By hand, |age| age > min_age is:
struct OlderThan<'a> { min_age: &'a u32 } // the capture: a shared borrow
impl OlderThan<'_> {
fn call(&self, age: u32) -> bool { age > *self.min_age } // the body, via &self
}
fn main() {
let min_age = 30;
let by_hand = OlderThan { min_age: &min_age };
let closure = |age: u32| age > min_age; // what the compiler writes for you
println!("{} {}", by_hand.call(36), closure(36));
}
true true
The field is the capture — a borrow that Lesson 08's struct lifetime keeps within min_age's life — and the method is the body. Only the receiver is left — Lesson 11's choice for every method — and the body makes it:
| The body… | needs | callable… | trait · method |
|---|---|---|---|
| only reads its captures | &self | any number of times, even when shared | Fn · call |
| changes a capture | &mut self | repeatedly, by its one holder | FnMut · call_mut |
| moves a capture out | self | once: the call consumes it | FnOnce · call_once |
Each receiver asks less of the caller than the next, so every Fn is also FnMut and every FnMut also FnOnce (std: Fn: FnMut: FnOnce); a function that takes a closure should ask for the weakest it can.
fn call_fn<F: Fn()>(f: F) { f(); f(); } // through &self, any number of times
fn call_fnmut<F: FnMut()>(mut f: F) { f(); f(); } // through &mut self
fn call_fnonce<F: FnOnce()>(f: F) { f(); } // by value: exactly once
fn main() {
let name = String::from("Ada");
let mut count = 0;
let report = String::from("done");
call_fn(|| println!("{name}")); // only reads -> Fn
call_fnmut(|| count += 1); // writes -> FnMut
call_fnonce(|| drop(report)); // gives it away -> FnOnce
call_fnonce(|| println!("{name}")); // an Fn is an FnOnce too
println!("{count}");
}
Ada Ada Ada 2
The program the rule excludes: called twice, a closure that gives its capture away would hand out a String it no longer has — Lesson 04's double free, one level up:
fn call_twice<F: Fn()>(f: F) { f(); f(); }
fn main() {
let report = String::from("done");
let send = || drop(report); // moves `report` out of the closure
call_twice(send);
}
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
5 | let send = || drop(report); // moves `report` out of the closure
| ^^ ------ closure is `FnOnce` because it moves the variable `report` out of its environment
And the dangling lambda, in Rust:
fn make_label_len() -> impl Fn() -> usize {
let label = String::from("config");
|| label.len() // borrows `label`, a local of this frame
}
fn main() {
let len = make_label_len();
println!("{}", len());
}
error[E0373]: closure may outlive the current function, but it borrows `label`, which is owned by the current function
help: to force the closure to take ownership of `label` (and any other referenced variables), use the `move` keyword
With move the closure takes everything its body mentions by value:
fn make_label_len() -> impl Fn() -> usize {
let label = String::from("config");
move || label.len() // the closure now owns `label`
}
fn main() {
let len = make_label_len();
println!("{} {}", len(), len()); // still Fn: called twice
}
6 6
The body still only reads label, so the closure is still Fn: move decides how a closure holds its variables, not what its body does. Its typical use is a thread's body, which may outlive its caller (Lesson 16).
FnOnce, E0373 the borrowed variable and the frame it would outlive. Neither knows what you meant. E0525's fix may be a weaker bound, a body that lends, or a clone() per call; E0373's move is wrong if you still need the variable — clone first, or keep the closure in its scope (for threads, Lesson 17's thread::scope). A suggestion is a local edit, not a design.Since edition 2021 a closure captures the places its body uses — stats.count, not stats — so the function may keep using another field (under edition 2018 rustc rejects this program):
struct Stats { count: u32, label: String }
fn main() {
let mut stats = Stats { count: 0, label: String::from("reads") };
let mut bump = || stats.count += 1; // uses only the field stats.count
println!("{}", stats.label); // another field, read meanwhile
bump();
println!("{} {}", stats.label, stats.count);
}
reads reads 1
A struct with one field per captured place has a size, and a type.
3 · Every closure has its own type
use std::mem::size_of_val;
fn main() {
let min_age: i32 = 30;
let ages = vec![36, 21, 45];
let owned = vec![36, 21, 45];
let nothing = |a: i32| a > 18; // captures nothing
let by_ref = |a: i32| a > min_age; // captures &min_age
let by_copy = move |a: i32| a > min_age; // captures a copy of min_age
let via_ref = || ages.len(); // captures &ages
let owns = move || owned.len(); // captures the Vec header itself
println!("{} {} {} {} {}", size_of_val(¬hing), size_of_val(&by_ref),
size_of_val(&by_copy), size_of_val(&via_ref), size_of_val(&owns));
}
0 8 4 8 24
Nothing captured: 0 bytes. min_age read by a plain closure: a pointer — a Copy value used by value in a closure is still captured by shared borrow — while move stores the 4-byte copy. ages by reference: a pointer; owned moved in: the Vec's 24-byte header (Lesson 03). (rustc 1.98.1, aarch64.) The type cannot be written, and it is unique even for identical text:
fn main() {
let step = 3;
let mut ops = vec![move |x: i32| x + step];
ops.push(move |x: i32| x + step); // the same text, a different type
}
= note: no two closures, even if identical, have the same type
Uniqueness makes calls cheap: the compiler knows which body f(x) runs, so the call is static and inlinable (Lesson 12). To name "some closure", pick a price: impl Fn(u32) -> bool (one hidden type), Box<dyn Fn(u32) -> bool> (different closures together, at Lesson 13's allocation and indirect call), or a fn(u32) -> bool pointer (closures that capture nothing). Rules this simple can be run by hand — so run them, against the compiler.
4 · Predict the captures
The widget's function owns a Copy value, an owner, a shared reference and an exclusive reference (binding not mut). Pick what the body does with each: every chosen variable is read, then used in its strongest way. The engine applies §1–§3 and agrees with rustc on all 384 closures the widget builds plus 800 random ones: 18,944 verdicts.
What to try. First preset, slider up from 0: each read adds a shared borrow and 8 bytes — 0, 8, 16, 24, 32 — and f stays Fn. Line 5, count += 1, makes count's capture exclusive and f only FnMut; the function's every use of count is now E0503. Line 6 moves report in: 48 bytes, only FnOnce. Line 7 writes through picked, whose binding is not mut, so rustc takes the Reference's unique immutable borrow — exclusive in effect, not writable in source — and reading or writing picked is E0501 (moving it, E0505). Press move: all by value, still 48 bytes and FnOnce; count turns ✓ (the closure has its own copy), picked E0382. The Copy trap: consume(count) only copies, so f stays Fn yet forbids count += 1 (E0506). A thread body: thread::spawn accepts it with move; without, E0373 for count and report.
F: FnMut() has no closure to inspect, only a bound, so passing it on where Fn is required is a trait error:
fn call_twice<F: Fn()>(f: F) { f(); f(); }
fn run<F: FnMut()>(step: F) {
call_twice(step); // step is only known to be FnMut
}
fn main() {
let mut count = 0;
run(|| count += 1);
}
error[E0277]: expected an `Fn()` closure, found `F`
= note: `F` implements `FnMut`, but it must implement `Fn`, which is more general
The note is §2's table in one line; whether run should promise more or call_twice ask less, rustc cannot say.Closures are behavior over a few variables. The commonest behavior-with-state walks a sequence: what does the walker hold?
5 · Iterators: the same three modes over a sequence
An iterator has one required method, next(&mut self) -> Option<Self::Item> (Lesson 09's Option; Item is an associated type); map, filter, sum, collect and the rest are provided on top. The receiver is &mut self because a cursor changes as it advances. A for loop hides that protocol — std documents it as IntoIterator::into_iter on the expression, then next until None:
fn main() {
let ages = vec![36, 21, 45];
for a in &ages { print!("{a} "); }
// what that for loop means:
let mut it = IntoIterator::into_iter(&ages); // borrows ages
loop {
match it.next() { // Option<&i32>
Some(a) => print!("{a} "),
None => break,
}
}
println!();
}
36 21 45 36 21 45
What into_iter receives is where ownership enters. A collection normally implements IntoIterator for itself, &C and &mut C — std's three forms of iteration, the subject of one of the most-voted Rust questions on Stack Overflow (534 votes as of 2026-09). They are the three modes again: iter() (for x in &v) yields &T, iter_mut() (&mut v) yields &mut T, into_iter() (plain v) yields T and consumes the collection:
fn main() {
let names = vec![String::from("ada"), String::from("linus")];
for n in names { print!("{n} "); } // the same as names.into_iter()
println!("{}", names.len()); // names was consumed by the loop
}
error[E0382]: borrow of moved value: `names`
note: `into_iter` takes ownership of the receiver `self`, which moves `names`
Since the iterator is a name holding a loan, Lesson 05's flagship needs no new rule: for a in &ages holds a shared loan on ages until its last next, across the body.
fn main() {
let mut ages = vec![36, 21, 45];
for a in &ages {
ages.push(*a); // may move the buffer under the iterator
}
}
error[E0502]: cannot borrow `ages` as mutable because it is also borrowed as immutable
3 | for a in &ages {
| -----
| |
| immutable borrow occurs here
| immutable borrow later used here
4 | ages.push(*a); // may move the buffer under the iterator
| ^^^^^^^^^^^^^ mutable borrow occurs here
Both labels sit on &ages, where the loan is created and where each iteration's next uses it. A shared name and an exclusive push may not overlap: no iterator invalidation, and no run-time check.
Adaptors join closures and iterators. map(f) runs nothing: it returns a Map struct holding the iterator and the closure, so ages.iter().map(f).filter(g) is a Filter around a Map around a slice iterator (take, zip, enumerate, rev alike). Nothing runs until a consumer — sum, collect, for — calls next:
fn main() {
let ages = vec![36, 21, 45];
let chain = ages.iter()
.map(|a| { println!(" map {a}"); a + 1 })
.filter(|b| { println!(" filter {b}"); *b > 30 });
println!("chain built; nothing has run");
let total: i32 = chain.sum();
println!("sum = {total}");
}
chain built; nothing has run map 36 filter 37 map 21 filter 22 map 45 filter 46 sum = 83
sum pulls from filter, which pulls from map, which pulls from the slice: each age travels the whole chain before the next is read — the loop you would write, as nested structs calling next. What does that cost?
6 · What the abstraction costs
A chain and a hand-written index loop computing the same sum, each kept a function of its own:
#[inline(never)]
pub fn chain(ages: &[i32], min_age: i32) -> i32 {
ages.iter().map(|a| a + 1).filter(|b| *b > min_age).sum()
}
#[inline(never)]
pub fn by_hand(ages: &[i32], min_age: i32) -> i32 {
let mut total = 0;
let mut i = 0;
while i < ages.len() {
let b = ages[i] + 1;
if b > min_age { total += b; }
i += 1;
}
total
}
With rustc 1.98.1 -C opt-level=3 --emit asm on aarch64 (Apple silicon, one machine, 2026-09) each is 81 instructions, and a diff finds one difference: two independent register-zeroing instructions, swapped. Neither contains a call — iter, map, filter, sum, both closures and every next were inlined — and both are vectorized (16, 4 and 1 ages per pass). The last two loops (comments mine):
chain (iter · map · filter · sum) by_hand (while loop, index)
LBB0_11: ; 4 ages per pass LBB1_11:
ldr q3, [x10], #16 ldr q3, [x10], #16
add.4s v3, v3, v2 ; b = a + 1 add.4s v3, v3, v2
cmgt.4s v4, v3, v0 ; b > min_age ? cmgt.4s v4, v3, v0
and.16b v3, v4, v3 ; keep b, or 0 and.16b v3, v4, v3
add.4s v1, v3, v1 ; total += b add.4s v1, v3, v1
adds x8, x8, #4 adds x8, x8, #4
b.ne LBB0_11 b.ne LBB1_11
LBB0_14: ; 1 age per pass LBB1_14:
ldr w11, [x9], #4 ldr w11, [x9], #4
add w12, w11, #1 add w12, w11, #1
cmp w12, w2 cmp w12, w2
csinc w11, wzr, w11, le ; b, or 0 csinc w11, wzr, w11, le
add w8, w11, w8 add w8, w11, w8
subs x10, x10, #1 subs x10, x10, #1
b.ne LBB0_14 b.ne LBB1_14
The filter closure became a compare and a branch-free select (csinc: b or 0), map's one add; no struct survived. The price is paid elsewhere: at -C opt-level=0 chain is four library calls (iter, map, filter, sum), so judge iterators optimized; and the optimizing costs compile time (Lesson 12 measured -O3 against -O0). Bounds checks, measured the same way:
#[inline(never)]
pub fn sum_indexed(ages: &[i32]) -> i32 {
let mut total = 0;
for i in 0..ages.len() { total += ages[i]; } // indexes: a check each time?
total
}
#[inline(never)]
pub fn sum_iter(ages: &[i32]) -> i32 {
let mut total = 0;
for a in ages { total += a; } // no index at all
total
}
#[inline(never)]
pub fn sum_first(ages: &[i32], n: usize) -> i32 {
let mut total = 0;
for i in 0..n { total += ages[i]; } // n may exceed ages.len()
total
}
sum_indexed and sum_iter contain no call to panic_bounds_check — the loop condition bounds every index, so the check was dropped — while sum_first has one, behind a single comparison of n with the length before its unchecked loop. The iterator wins not by removing checks but by having no index to get wrong.
7 · Two sidebars, and the seam
Two signatures in this lesson carry a lifetime question that Lesson 08 could not yet ask: what a closure bound promises, and what next may return.
F: Fn(&str) -> &str the caller of f picks the borrow, so f must work for every lifetime: the bound is sugar for the higher-ranked for<'a> Fn(&'a str) -> &'a str, and both spellings accept the same closure:
fn apply<F: Fn(&str) -> &str>(f: F) -> usize { f("ada lovelace").len() }
fn apply_hr<F>(f: F) -> usize where F: for<'a> Fn(&'a str) -> &'a str { f("ada lovelace").len() }
fn main() { println!("{} {}", apply(|s| &s[..3]), apply_hr(|s| &s[..3])); }
3 3A closure written alone gets no such signature: rustc gives its parameter and its result separate lifetimes (
'1 and '2 in the message), so this is rejected, though the same body passed to apply compiles:
fn main() {
let first = |s: &str| s.split(' ').next().unwrap_or(s); // returns a borrow of s
println!("{}", first("ada lovelace"));
}
error: lifetime may not live long enough
| | return type of closure is &'2 str
| let's call the lifetime of this reference `'1`next(&mut self) -> Option<Self::Item> cannot hand out an item borrowed from the iterator itself: Item is one type for the whole iterator and cannot name each call's &mut self lifetime. Generic associated types, stable since Rust 1.65, are the later fix (a "lending iterator").Note the owners: every variable here had exactly one — the function, or the closure after a move — and every borrow was checked before the program ran.
Common mistakes / failure modes
fn pointer is a code address; a closure is a struct of captures. Only one that captures nothing coerces to a fn pointer (§3).move makes a closure FnOnce"move || label.len() is Fn (§2); the widget's toggle never changes the trait.Box<dyn Fn…>, puts it on the heap.opt-level=3, two swapped; at opt-level=0 the chain is four calls. Measure optimized.next (the §5 trace).iter() and into_iter() are the same"iter() lends &T; a collection's into_iter() consumes it: after for n in names, names is gone (§5).Checkpoint exercise
count unused, report mutate, ages read, picked unused, move off. Predict the captures, trait, size, the verdicts for report and for spawn; then what move changes. (Answer: report exclusive, ages shared; FnMut; 16 bytes; report: read E0502, mutate E0499, move E0505; spawn: E0373 for report, E0597 for data. With move: 32 bytes, still FnMut, E0382 for all of report, spawn rejected by E0597 alone — move copied ages, not the data it points into; with ages unused spawn accepts, at 24 bytes.) Then compile that move version with the hint's declarations and std::thread::spawn(f);: rustc should name data.Where this points next
Every capture here had one owner behind it and a borrow checked before the program ran. Real programs strain both. A callback registered with a button and a log needs each to keep it alive: two owners. An Fn closure that counts its calls hits §2's table: Fn may only read its captures, and counting writes. One needs a value with several owners; the other, a change through a handle others share, which the compile-time law forbids. So what about data that genuinely has several owners, or must change through a shared handle?
Copy value is only read). The body fixes the receiver, and the receiver is the trait: Fn (&self), FnMut (&mut self), FnOnce (self), each implying the next; move changes the holding, never the trait. Each closure is its own type, as big as its captures, called statically. Iterators are the same modes over a sequence (iter, iter_mut, into_iter), driven by next(&mut self) through lazy adaptors; invalidation is a compile error, and in the build measured a chain compiled like the hand loop.Interview prompts
- What decides whether a closure is
Fn,FnMutorFnOnce? (§2 — what the body does with its captures: reading needs&self, changing one&mut self, moving one outself; each trait implies the next.) - Does
movemake a closureFnOnce? (§2 — no: captures become by value, but the trait follows the body;move || label.len()isFn.) - How big is a closure, and why can't one
Vechold two of the same signature? (§3 — as big as its captures (0 for none, a pointer per borrow, the value when moved in), and each has its own type: box them asdyn Fn.) - Why does a returned closure, or one given to
thread::spawn, usually needmove? (§2, §4 — otherwise it borrows locals of a frame it may outlive (E0373); a reference copied in still points into that frame (E0597).) - Explain
iter(),iter_mut(),into_iter(), and why you cannot push to aVecyou iterate. (§5 — the three modes:&T,&mut T,T; the loop's iterator holds a shared loan until its lastnext, sopushis E0502.) - In what sense are adaptors zero-cost, and how would you check? (§6 — lazy structs with static calls the optimizer inlines: in one optimized build chain and loop were the same 81 instructions, two swapped; at
opt-level=0, four calls. Read optimized assembly.)
Companion reads: C++ · 14 STL algorithms and ranges, FP · 13 Laziness and streams, Go · 08 Goroutines (a closure-capture bug).