Closures
In one sentence
Section titled “In one sentence”A closure |a, b| a + b is a Java lambda that can also change the local variables it captures, and its type says whether it reads, changes, or consumes them.
The Java you know
Section titled “The Java you know”A Java lambda can only read local variables that are “effectively final”. To count inside a lambda, Java code uses a workaround such as AtomicInteger or a one-element array:
Java
static int countBig(List<Long> prices) { long limit = 1_000; // effectively final: the lambda can read it AtomicInteger big = new AtomicInteger(); // an `int` cannot change inside a lambda prices.forEach(p -> { if (p > limit) { big.incrementAndGet(); } }); return big.get();}Rust
pub fn count_big(prices: &[u64]) -> (u32, Vec<u64>) { let mut big = 0; // a local variable, not "effectively final" let limit = 1_000; let mut discounted = Vec::new(); let mut record = |p: u64| { if p > limit { big += 1; // the closure changes `big`: a Java lambda cannot do this discounted.push(p - p / 10); } }; for &p in prices { record(p); } (big, discounted)}The Rust closure changes big and discounted directly. The borrow checker makes this safe: while record exists, it holds a mutable borrow of both variables, so no other code can use them at the same time.
The Rust you will see
Section titled “The Rust you will see”pub fn syntax() -> (i32, u64, u64) { let add = |a, b| a + b; // types inferred: here `i32`, from the call below let add_typed = |a: u64, b: u64| -> u64 { a + b }; // types written out let with_vat = |cents: u64| { let vat = cents * 8 / 100; // a block body, like a Java lambda with `{ }` cents + vat }; (add(1, 2), add_typed(3, 4), with_vat(1_000))}- The parameters are between
|and|.||is a closure with no parameters. - The types are usually inferred. When they are written, the syntax is the same as for functions.
- The body is one expression, or a block
{ ... }whose last expression is the result. - A function name also works where a closure is expected:
.map(str::trim)is.map(|s| s.trim()), like a Java method reference.
How to read it
Section titled “How to read it”Fn, FnMut, FnOnce
Section titled “Fn, FnMut, FnOnce”Java has one kind of lambda, with many functional interfaces (Function, Consumer, Supplier). Rust has three traits, and they say how the closure uses what it captured:
/// `Fn`: the closure only reads what it captured. It can be called many times.pub fn apply_to_all(prices: &[u64], rule: impl Fn(u64) -> u64) -> Vec<u64> { prices.iter().map(|&p| rule(p)).collect()}
/// `FnMut`: the closure changes what it captured. It can be called many times, one at a time.pub fn for_each_price(prices: &[u64], mut visit: impl FnMut(u64)) { for &p in prices { visit(p); }}
/// `FnOnce`: the closure may consume what it captured. It can be called only once.pub fn finish(on_done: impl FnOnce() -> String) -> String { on_done()}| Trait | The closure | Can be called |
|---|---|---|
Fn |
only reads its captured variables | many times, also at the same time |
FnMut |
changes its captured variables | many times, one call at a time |
FnOnce |
may move a captured value out | once |
The compiler chooses the trait from the body of the closure. The caller does not write it:
pub fn fn_traits_use() -> (Vec<u64>, u64, String) { let rate = 10; let discounted = apply_to_all(&[1_000, 2_000], |p| p - p * rate / 100); // reads `rate`: `Fn`
let mut total = 0; for_each_price(&[1_000, 2_000], |p| total += p); // changes `total`: `FnMut`
let report = String::from("2 prices"); let message = finish(move || report); // gives `report` away: `FnOnce`
(discounted, total, message)}- Line 3: The closure reads
rate, so it isFn.apply_to_allcan call it for each price. - Line 6: The closure changes
total, so it isFnMut.totalis borrowed mutably untilfor_each_pricereturns. - Line 9:
moveputsreportinside the closure, and the closure returns it. Returning a captured value moves it out, so the closure isFnOnce.
When you read a function signature, the trait tells you what the function will do with the closure. impl Fn means “I may call it many times”. impl FnOnce means “I call it at most once”.
move closures
Section titled “move closures”By default, a closure borrows what it captures. move makes it take ownership instead. You see move for threads and async tasks, because they can run after the function that created them has returned:
pub fn total_in_background(prices: Vec<u64>) -> u64 { let handle = thread::spawn(move || { // `move`: the closure owns `prices`. The thread can outlive this function, // so it cannot borrow from it. prices.iter().sum::<u64>() }); handle.join().expect("the thread does not panic")}Without move, this does not compile (error E0373): the closure would borrow prices, but the thread may outlive it. With move, the closure owns prices. For values that several threads need, the code clones an Arc before the closure, and moves the clone (see Smart pointers).
Returning a closure
Section titled “Returning a closure”A function can return a closure. Each closure has its own unnamed type, so the return type is impl Fn(...):
/// Returns a closure. `impl Fn` is a type that the compiler knows, but the code does not name.pub fn percent_off(percent: u64) -> impl Fn(u64) -> u64 { move |cents| cents - cents * percent / 100}move is necessary here too: the closure must own percent, because percent_off returns before the closure is called. Box<dyn Fn(u64) -> u64> also works, and is needed when one function can return different closures or when closures of different types go in one collection.
What good looks like
Section titled “What good looks like”impl Fnor a genericF: Fnin parameters.Box<dyn Fn>only to store closures of different types together.- The weakest trait that works in a signature:
FnOnceaccepts the most closures, thenFnMut, thenFn. movewith a clear reason: a thread, a task, or a returned closure.- Short closures. If a closure is longer than a few lines, a named function is often easier to read and to test.
- Function names instead of trivial closures:
.map(String::as_str),.filter(Option::is_some).
Red flags in AI output
Section titled “Red flags in AI output”moveplusclone()of large data for every closure: the closure gets a full copy where a borrow, or a clone of anArc, is enough. See S01:clone()to satisfy the borrow checker.Box<dyn Fn>in parameters and return types whereimpl Fnworks. See S22:Box<dyn Trait>where a generic or enum is sufficient.Rc<RefCell<...>>to change a variable inside a closure, a translation of the JavaAtomicIntegerhabit. A plainlet mutand anFnMutclosure are enough.- Long closures with nested closures inside, often in iterator chains.
Better vs worse
Section titled “Better vs worse”The task: build three labelling rules from a catalog.
WorseEach closure gets its own full copy of the catalog names, in a
Box<dyn Fn>.applytakes the box by value.pub struct Catalog {pub names: Vec<String>,}pub fn build_rules(catalog: &Catalog) -> Vec<Box<dyn Fn(u64) -> String>> {let mut rules: Vec<Box<dyn Fn(u64) -> String>> = Vec::new();for i in 0..3 {let names = catalog.names.clone(); // a full copy of the catalog for each closurerules.push(Box::new(move |cents| format!("{}: {cents}", names[i])));}rules}pub fn apply(rule: Box<dyn Fn(u64) -> String>, cents: u64) -> String {rule(cents)}Run in Playground (opens the Rust Playground in a new tab) BetterEach closure borrows only the name it needs.
impl Fnin the return type and the parameter: no box, no copy.pub struct Catalog {pub names: Vec<String>,}/// Each closure borrows only the name it needs. The result cannot outlive the catalog.pub fn build_rules(catalog: &Catalog) -> Vec<impl Fn(u64) -> String + '_> {catalog.names.iter().take(3).map(|name| move |cents| format!("{name}: {cents}")).collect()}pub fn apply(rule: impl Fn(u64) -> String, cents: u64) -> String {rule(cents)}Run in Playground (opens the Rust Playground in a new tab)
The “worse” version is a common way to fix “borrowed value does not live long enough” errors: clone, then move. It works, as the test both_rule_sets_agree shows, but each rule copies the whole catalog to use one name. With a catalog of 10 000 names and 100 rules, that is a million String copies.
The “better” version keeps the borrow. impl Fn(u64) -> String + '_ says that the closures borrow from the catalog, so the compiler makes sure that the catalog lives as long as the rules. If the rules must outlive the catalog, an Arc<[String]> shared by all closures is the next step, not a copy per closure.
Check yourself
Section titled “Check yourself”Can a Rust closure change a local variable that it captures?
Show the answer
Yes. The closure is then
FnMut, and it holds a mutable borrow of the variable while it exists. Java lambdas cannot do this: captured variables must be effectively final.Why does
thread::spawnalmost always take amoveclosure?Show the answer
The thread can run longer than the function that started it. A closure that borrows local variables could use them after they are gone, so the compiler requires the closure to own them (error E0373 otherwise).
A function takes
f: impl FnOnce() -> Report. How many times can it callf?Show the answer
At most once.
FnOnceclosures may consume what they captured, so a second call is not possible.A struct field is
Box<dyn Fn(u64) -> u64>, and the struct holds one closure that never changes type. What could it be instead?Show the answer
A generic field:
struct Pricer<F: Fn(u64) -> u64> { rule: F }. That avoids the heap allocation and the dynamic call. The box is needed when different closure types must be stored in the same field or collection.