Rust Closures

Closure Syntax

Anonymous functions that capture their environment
// Full syntax
let add = |a: i32, b: i32| -> i32 { a + b };

// Type inference — usually no annotations needed
let add = |a, b| a + b;
let result = add(2, 3);  // compiler infers i32

// Multi-line closure
let process = |name: &str| {
    let upper = name.to_uppercase();
    println!("Processing: {upper}");
    upper
};

Closures infer parameter and return types from usage. Once a closure is called with specific types, those types are fixed.

Capturing Variables

Closures capture variables from their environment
let name = String::from("Evan");

// Borrow immutably (Fn)
let greet = || println!("Hello, {name}");
greet();
println!("{name}");  // name still valid

// Borrow mutably (FnMut)
let mut count = 0;
let mut increment = || { count += 1; };
increment();
increment();
println!("{count}");  // 2

// Take ownership (FnOnce)
let name = String::from("Evan");
let consume = move || {
    println!("consumed: {name}");
    drop(name);
};
consume();
// name is no longer valid here

The compiler infers the minimal capture mode. move forces ownership transfer — required when the closure outlives the captured variables (threads, async).

Closure Traits

Fn, FnMut, FnOnce — the closure hierarchy
// Fn — borrows immutably, can be called multiple times
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
    f(f(x))
}
let doubled = apply_twice(|x| x * 2, 3);  // 12

// FnMut — borrows mutably, can be called multiple times
fn apply_to_each(mut f: impl FnMut(i32), items: &[i32]) {
    for &item in items {
        f(item);
    }
}

// FnOnce — may consume captured data, can be called only once
fn execute(f: impl FnOnce() -> String) -> String {
    f()
}

Fn is a subset of FnMut, which is a subset of FnOnce. A function accepting FnOnce is the most flexible — it accepts any closure. A function accepting Fn is the most restrictive.

Closures with Iterators

map, filter, and friends take closures
let numbers = vec![1, 2, 3, 4, 5];

// map with closure
let squares: Vec<i32> = numbers.iter().map(|x| x * x).collect();

// filter with closure
let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();

// sort_by with closure
let mut names = vec!["charlie", "alice", "bob"];
names.sort_by(|a, b| a.len().cmp(&b.len()));

// for_each — closure instead of for loop
numbers.iter().for_each(|x| println!("{x}"));

Returning Closures

impl Fn or Box<dyn Fn>
// Return closure with impl (when type is known at compile time)
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}
let add5 = make_adder(5);
println!("{}", add5(3));  // 8

// Return boxed closure (when type varies at runtime)
fn make_formatter(prefix: &str) -> Box<dyn Fn(&str) -> String + '_> {
    Box::new(move |msg| format!("[{prefix}] {msg}"))
}

impl Fn uses static dispatch. Box<dyn Fn> uses dynamic dispatch — needed when the concrete type differs between branches.

move Closures

Transfer ownership into the closure
use std::thread;

let data = vec![1, 2, 3];

// move is required — thread may outlive the current scope
let handle = thread::spawn(move || {
    println!("{:?}", data);
});
// data is no longer accessible here
handle.join().unwrap();

move transfers ownership of all captured variables into the closure. Required for closures sent to threads or returned from functions.