Rust Closures

A closure is a function you can write inline and store in a variable. Unlike regular functions, closures can capture variables from the surrounding code. They are especially useful with iterators, event handlers, and any situation where you need a short, throwaway piece of logic.

Writing a Closure

let add = |x, y| x + y;

println!("{}", add(3, 4));   ← 7

The vertical bars |x, y| contain the parameters. The expression after them is the body. No braces needed for a single expression.

Closure vs Function Comparison

fn add_fn(x: i32, y: i32) -> i32 { x + y }   ← Regular function
let add_cl = |x: i32, y: i32| -> i32 { x + y };  ← Closure, explicit types
let add_cl = |x, y| x + y;                        ← Closure, types inferred

The Sticky Note Diagram

Regular function = A recipe in a cookbook
                   Anyone can use it, always available

Closure          = A sticky note with instructions
                   Stays near the code that uses it
                   Can refer to things nearby (captured variables)

Capturing the Environment

A closure can use variables from the scope where it is defined. Regular functions cannot do this.

fn main() {
    let threshold = 10;

    let is_big = |n| n > threshold;   ← Captures threshold from outer scope

    println!("{}", is_big(15));  ← true
    println!("{}", is_big(5));   ← false
}

threshold is not a parameter of the closure — the closure captures it from the environment automatically.

How Closures Capture

Rust closures capture variables in three ways depending on how the closure uses them:

Capture Mode    Keyword    When Used
------------    -------    ---------
By reference    (auto)     Read-only access to the captured value
By mutable ref  (auto)     Needs to modify the captured value
By move         move       Takes ownership of the captured value

Capture by Reference (Default)

let greeting = String::from("Hello");
let say_hi = || println!("{}", greeting);   ← Borrows greeting
say_hi();
println!("{}", greeting);   ← greeting still works here

Capture by Move

let greeting = String::from("Hello");
let say_hi = move || println!("{}", greeting);   ← Takes ownership
say_hi();
// greeting is no longer usable here

Move closures are important when passing closures to threads, since the thread might outlive the current scope.

Closures as Function Parameters

Pass a closure to a function using one of the three function traits:

Trait     Meaning
-----     -------
Fn        Can be called many times, borrows the environment
FnMut     Can be called many times, mutably borrows the environment
FnOnce    Can be called only once, takes ownership of the environment
fn apply<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
    f(value)
}

fn main() {
    let double = |x| x * 2;
    println!("{}", apply(double, 5));   ← 10
}

Closures with Iterators

Closures shine when used with iterator methods. These methods accept closures to express what to do with each element.

map — Transform Each Element

let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);   ← [2, 4, 6, 8, 10]

filter — Keep Only Matching Elements

let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
println!("{:?}", evens);   ← [2, 4, 6]

The Assembly Line Diagram

numbers: [1, 2, 3, 4, 5]
           ↓ map(|n| n * 2)
doubled: [2, 4, 6, 8, 10]
           ↓ filter(|&&n| n > 5)
result:  [6, 8, 10]

Returning Closures

Return a closure from a function using impl Fn or boxing:

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    move |x| x * factor
}

fn main() {
    let triple = make_multiplier(3);
    println!("{}", triple(5));   ← 15
}

Quick Reference

|x| x + 1                   ← Simple closure
|x: i32| -> i32 { x + 1 }  ← Closure with explicit types
move |x| x + captured_val   ← Moves captured variables
fn f<F: Fn(i32)>(f: F)     ← Accept a closure as parameter

Leave a Comment