Closures
Capture by borrow when you only look. Steal with move when ownership must leave.
<!-- hal:authoritative:yaml -->
*Store a short anonymous function in a variable or pass it as an argument. Capture by borrow when you only need to look, and steal with move when ownership must leave this scope.*
§I — Frame
Duha session 14. TRPL Chapter 13 opens functional features; the syllabus splits it. Today is closures only. Session 15 owns iterators. Session 13 left you with minigrep args, file read, and a library/binary split. Keep that project in mind; do not rebuild it here.
Three moves land by the end:
- Write a closure: pipes for parameters, optional types, call it like a function.
- Capture honestly: immutable borrow, mutable borrow, or
moveownership. - Name the trait bound:
FnOnce,FnMut,Fnas what callers can demand.
Syllabus Done-criteria: Can write a closure that borrows and one that steals.
§II — Closures capture; functions do not
A closure is an anonymous function you can store or pass. Unlike fn items, it can capture values from the surrounding scope.
The book's shirt giveaway shows why that matters. Option::unwrap_or_else takes a closure that runs only on None. The inventory method passes || self.most_stocked(): no parameters between the pipes, body calls a method on self. The standard library never heard of Inventory. The closure carries an immutable borrow of self into unwrap_or_else.
user_preference.unwrap_or_else(|| self.most_stocked())
A plain function could not reach self without an extra parameter. Capture is the point.
Type annotations on closures are optional. The compiler infers parameter and return types from use, then locks them. Call once with String and again with i32 on the same closure variable and you get a type error. Annotate when clarity beats brevity:
let expensive = |num: u32| -> u32 {
// ...
num
};
Syntax ladder from the book: fully typed closure, types dropped, braces dropped for a one-expression body. Same call behavior.
§III — Borrow, mutate, or steal
Capture follows what the body does with the value, matching how functions take parameters.
Immutable borrow (Listing 13-4). The body only prints list. The closure captures &list. You can still use list before and after the call:
let list = vec![1, 2, 3];
let only_borrows = || println!("From closure: {list:?}");
only_borrows();
println!("After: {list:?}");
Mutable borrow (Listing 13-5). The body pushes into list. The closure captures &mut list. Between definition and call you cannot also take an immutable borrow for println!. The mutable capture occupies the value until the last use of that closure.
**Steal with move** (Listing 13-6). Force ownership even when the body would only need a borrow. Threads are the book's first hard case: the new thread may outlive the stack frame that defined the closure, so the compiler requires move so list is owned by the thread's closure:
use std::thread;
let list = vec![1, 2, 3];
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
Remove move or use list in the main thread after the spawn and the compiler rejects it. That is the steal the Done-criteria asks you to write: ownership leaves, the old name is gone.
§IV — FnOnce, FnMut, Fn
How the body treats captured values picks which traits the closure implements:
| Trait | Meaning |
|---|---|
FnOnce | Callable at least once. Closures that move a captured value out implement only this. |
FnMut | May mutate captures; may be called more than once. |
Fn | Neither moves out nor mutates; safe to call repeatedly, including concurrently. |
unwrap_or_else bounds its closure as FnOnce() -> T. It calls the closure at most once, so every closure kind is accepted.
sort_by_key bounds as FnMut because it calls the key function once per element. |r| r.width works. A closure that pushes a String out of the environment into another vec implements only FnOnce and fails that bound (Listing 13-8). Count calls with a counter you mutate, not with a moved String.
Functions that capture nothing also implement the Fn family. You can pass Vec::new where a FnOnce is wanted.
§V — One complete proof
- Write
|x| x + 1(or typed|x: u32| -> u32 { x + 1 }) and call it. - Borrow: closure that only reads a
Vecvia immutable capture; print the vec after the call. - Steal:
moveaVecinto athread::spawnclosure (or any API that needs ownership) and confirm the outer name cannot be used afterward. - Pass a borrow-style closure to
unwrap_or_elseonOption. - Pass
|r| r.widthtosort_by_key; say why a move-out-of-env closure would not compile there.
When those five hold, session 14's selected depth is done. Iterators wait for session 15.
§VI — Closing
Closures are functions that remember their neighborhood. Borrow when you only look. Mutate when you change in place. move when ownership must leave this frame. Callers name FnOnce / FnMut / Fn so the type system matches how often and how roughly they will call you. Next fire: iterators on the same chapter.
Done-criteria: Can write a closure that borrows and one that steals.