Skip to content
AgileLogics
5 min read

Closures

Prerequisites: Lifetimes

A closure is an anonymous function you can store in a variable or pass to another function. Unlike a normal fn, a closure can capture variables from the scope where it is defined.

Syntax

fn main() {
    let add_one = |x: i32| -> i32 { x + 1 };  // fully annotated
    let add_two = |x| x + 2;                  // types inferred, braces optional for one expression
    let say_hi = || println!("hi");           // no parameters

    println!("{} {}", add_one(1), add_two(1));
    say_hi();
}

Parameter and return types are usually inferred from how the closure is used. Once inferred, they are fixed: a closure has exactly one set of types.

Capturing

A closure captures each variable in the least demanding way that works:

  1. By shared reference (&T) if it only reads the variable.
  2. By mutable reference (&mut T) if it modifies it.
  3. By value (moving it in) if it needs ownership, for example to drop it or return it.

Writing move before the closure forces capture by value for everything it uses. That is required when the closure must outlive the current scope, such as when it is returned from a function or sent to another thread.

Fn, FnMut and FnOnce

How a closure uses its captures decides which traits it implements, and those traits are how functions accept closures:

Trait Can be called Closure body...
FnOnce at least once may move captured values out (every closure is FnOnce)
FnMut many times may modify captured values
Fn many times, even concurrently only reads captured values

When writing a function that takes a closure, ask for the least restrictive trait you need: FnOnce if you call it once, FnMut if you call it repeatedly, and Fn only if you need to call it through a shared reference.

Example

fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(f(x))
}

fn repeat(times: usize, mut action: impl FnMut()) {
    for _ in 0..times {
        action();
    }
}

fn consume(f: impl FnOnce() -> String) -> String {
    f()
}

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    move |x| x * factor   // `move`: the closure keeps its own copy of `factor`
}

fn main() {
    // Fn: reads `offset` by shared reference.
    let offset = 10;
    let add_offset = |x| x + offset;
    println!("{}", apply_twice(add_offset, 1));

    // FnMut: modifies `count` through a mutable reference.
    let mut count = 0;
    repeat(3, || count += 1);
    println!("count = {count}");

    // FnOnce: moves `name` out of the closure, so it can only run once.
    let name = String::from("Ferris");
    let greeting = consume(move || name + " says hi");
    println!("{greeting}");

    // Returning a closure.
    let triple = make_multiplier(3);
    println!("{}", triple(7));

    // Closures stored in a Vec need a common type: Box<dyn Fn>.
    let ops: Vec<(&str, Box<dyn Fn(i32) -> i32>)> = vec![
        ("double", Box::new(|x| x * 2)),
        ("square", Box::new(|x| x * x)),
        ("negate", Box::new(|x| -x)),
    ];
    for (label, op) in &ops {
        println!("{label}(5) = {}", op(5));
    }

    // Closures are what most iterator and Option methods take.
    let mut words = vec!["pear", "fig", "banana"];
    words.sort_by_key(|w| w.len());
    println!("{words:?}");
}

Expected output

21
count = 3
Ferris says hi
21
double(5) = 10
square(5) = 25
negate(5) = -5
["fig", "pear", "banana"]

apply_twice(add_offset, 1) computes (1 + 10) + 10 = 21.

Common mistakes

  • Using one closure with two different types: let id = |x| x; id(5); id("five"); fails with error[E0308]: mismatched types, because the first call fixed x as an integer.

  • Calling a closure that mutates a capture without declaring the closure variable mut:

    fn main() {
        let mut total = 0;
        let add = |n: i32| total += n;
        add(5);
        println!("{total}");
    }
    

    This fails with error[E0596]: cannot borrow `add` as mutable, as it is not declared as mutable. Write let mut add = ....

  • Using a captured variable while a closure still holds a mutable borrow of it. The borrow lasts until the closure's last use, so finish with the closure first.

  • Forgetting move when returning a closure that captures a local: error[E0373]: closure may outlive the current function, but it borrows `factor`, which is owned by the current function.

Practice

Write fn make_counter() -> impl FnMut() -> u32 that returns a closure which returns 1, 2, 3, … on successive calls. Call it three times and print the results on one line.

Show solution
fn make_counter() -> impl FnMut() -> u32 {
    let mut count = 0;
    move || {
        count += 1;
        count
    }
}

fn main() {
    let mut next = make_counter();
    let a = next();
    let b = next();
    let c = next();
    println!("{a} {b} {c}");
}
1 2 3