all_lessons/Rust/14 · Closures and iteratorslesson 15 / 23

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.

The thesis, here
Closures add no safety rule. A capture is a borrow or a move stored in a struct field, so the law and the regions of Lessons 05–08 apply unchanged. What closures add is inference: for each variable the compiler picks the weakest mode its uses allow.
Linear position
Forced by: Static and dynamic dispatch abstract over types. What about abstracting over behavior passed as a value — a piece of code together with the variables it uses? What can the variables it uses mean when every variable has an owner?
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?
The plan
(1) What "uses a variable" can mean. (2) A closure as a struct, its trait as a receiver. (3) Its type and size. (4) Predict captures, checked by rustc. (5) Iterators: the same modes, lazily. (6) The cost, in assembly. (7) Sidebars, and the seam.

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…needscallable…trait · method
only reads its captures&selfany number of times, even when sharedFn · call
changes a capture&mut selfrepeatedly, by its one holderFnMut · call_mut
moves a capture outselfonce: the call consumes itFnOnce · 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).

Reading the error
Both messages name the capture that decided the mode: E0525 the use that made the closure 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(&nothing), 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 does this closure capture?
let mut count: i32 = 0; let mut report = String::from("ok"); let ages: &Vec<i32> = &data; let picked: &mut Vec<i32> = &mut chosen; and fn consume<T>(_x: T) {}.
strongest trait
—
captures
—
size of f
—
thread::spawn(f)
—
Show the core JS
function need(v, op) {
  if (v.cls === 'i32')     // a Copy value: passing it by value is only a read
    return { mode: op === 'mutate' ? 'exclusive' : 'shared', path: 'var' };
  if (v.cls === 'String')
    return { mode: { read: 'shared', mutate: 'exclusive', move: 'value' }[op], path: 'var' };
  if (v.cls === 'ref')     // ages.len() reads *ages; consume(ages) copies ages itself
    return { mode: 'shared', path: op === 'read' ? 'deref' : 'var' };
  // picked (&mut, binding not mut): read through it, write through it, or give it away
  return { mode: { read: 'shared', mutate: 'unique', move: 'value' }[op], path: op === 'move' ? 'var' : 'deref' };
}

function captures(lines, isMove) {
  var st = {};
  VARS.forEach(function (v) { st[v.name] = { mode: 'none', path: null, line: -1 }; });
  lines.forEach(function (ln, i) {
    var s = st[ln.v];
    var n = isMove ? { mode: 'value', path: 'var' } : need(BY[ln.v], ln.op);  // move: all by value
    if (RANK[n.mode] > RANK[s.mode]) { s.mode = n.mode; s.line = i; }
    if (s.path !== 'var') s.path = n.path;
  });
  return st;
}

function traitOf(lines) {
  var once = [], mut = [];
  lines.forEach(function (ln, i) {
    if (ln.op === 'move' && !BY[ln.v].copy) once.push(i);   // gives an owned capture away
    if (ln.op === 'mutate') mut.push(i);                     // changes a capture
  });
  if (once.length) return { trait: 'FnOnce', why: once, Fn: false, FnMut: false, FnOnce: true };
  if (mut.length) return { trait: 'FnMut', why: mut, Fn: false, FnMut: true, FnOnce: true };
  return { trait: 'Fn', why: [], Fn: true, FnMut: true, FnOnce: true };
}

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.

E0277 · the bound that is too weak
A generic function that knows only 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.

What this measurement does not show
One function, one compiler, one machine — not a law. The Book's own benchmark put its two versions within about 2%, inside its error bars, and calls one benchmark not comprehensive. Measure yours, optimized.

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.

Sidebar · higher-ranked bounds
In 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 3
A 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`
Sidebar · items that borrow from the iterator
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

"Closures are like function pointers"
A 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"
It changes how captures are held, not the trait: move || label.len() is Fn (§2); the widget's toggle never changes the trait.
"Closures always allocate"
A closure is a plain value as big as its captures (0, 8, 4, 8, 24 bytes in §3); only boxing one, as in Box<dyn Fn…>, puts it on the heap.
"Iterators are slower than loops"
In §6 both gave the same instructions at opt-level=3, two swapped; at opt-level=0 the chain is four calls. Measure optimized.
"Adaptors do the work when called"
They only build structs; nothing runs until something calls 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

Try it
Set 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?

Takeaway
A closure is an anonymous struct of the places its body uses, each held in one of the three ownership modes: shared borrow to read, exclusive borrow to write, by value to give away (a 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

Companion reads: C++ · 14 STL algorithms and ranges, FP · 13 Laziness and streams, Go · 08 Goroutines (a closure-capture bug).