Skip to content

Closures

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.

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.

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.

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)
}
  1. Line 3: The closure reads rate, so it is Fn. apply_to_all can call it for each price.
  2. Line 6: The closure changes total, so it is FnMut. total is borrowed mutably until for_each_price returns.
  3. Line 9: move puts report inside the closure, and the closure returns it. Returning a captured value moves it out, so the closure is FnOnce.

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”.

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).

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.

  • impl Fn or a generic F: Fn in parameters. Box<dyn Fn> only to store closures of different types together.
  • The weakest trait that works in a signature: FnOnce accepts the most closures, then FnMut, then Fn.
  • move with 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).
  • move plus clone() of large data for every closure: the closure gets a full copy where a borrow, or a clone of an Arc, is enough. See S01: clone() to satisfy the borrow checker.
  • Box<dyn Fn> in parameters and return types where impl Fn works. 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 Java AtomicInteger habit. A plain let mut and an FnMut closure are enough.
  • Long closures with nested closures inside, often in iterator chains.

The task: build three labelling rules from a catalog.

  1. WorseEach closure gets its own full copy of the catalog names, in a Box<dyn Fn>. apply takes 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 closure
    rules.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)
  2. BetterEach closure borrows only the name it needs. impl Fn in 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.

  1. 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.

  2. Why does thread::spawn almost always take a move closure?

    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).

  3. A function takes f: impl FnOnce() -> Report. How many times can it call f?

    Show the answer

    At most once. FnOnce closures may consume what they captured, so a second call is not possible.

  4. 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.