12.1 Defining and Using Closures

A closure is essentially a function you can define inline, without a name, which automatically “closes over” or captures variables from its surrounding environment. A closure definition begins with vertical pipes (|...|) enclosing the parameters and can appear anywhere an expression is valid. Because it is an expression, you can store it in a variable, return it from a function, or pass it to another function—just like any other value. Closures are called using the standard function call syntax ().

Key Characteristics:

  • Anonymous: Closures don’t require a name, though they can be assigned to variables.
  • Environment Capture: They can access variables from the scope where they are created.
  • Concise Syntax: Parameter and return types can often be inferred.

12.1.1 Syntax: Closures vs. Functions

While similar, closures have a more flexible syntax than named functions.

Named Function Syntax:

#![allow(unused)]
fn main() {
fn add(x: i32, y: i32) -> i32 {
    x + y
}
}

Closure Syntax:

#![allow(unused)]
fn main() {
let add = |x: i32, y: i32| -> i32 {
    x + y
};
// Called like a function: add(5, 3)
}

If the closure body is a single expression, the surrounding curly braces {} are optional:

fn main() {
    let square = |x: i64| x * x; // Braces omitted
    println!("Square of {}: {}", 7, square(7)); // Output: Square of 7: 49
}

A closure taking no arguments uses empty pipes || as the syntax element identifying it as a closure with zero parameters:

fn main() {
    let message = "Hello!";
    let print_message = || println!("{}", message); // Captures 'message'
    print_message(); // Output: Hello!
}

Parameter and return types can often be omitted if the compiler can infer them:

fn main() {
    let add_one = |x| x + 1; // Types inferred (likely i32 -> i32 here)
    let result = add_one(5);
    println!("Result: {}", result); // Output: Result: 6
}

Key Differences Summarized:

AspectFunctionClosure
NameMandatory (fn my_func(...))Optional (can assign to let my_closure = ...)
Parameter / Return TypesMust be explicitInferred when possible
Environment CaptureNot allowedAutomatic by reference, mutable ref, or move
Implementation DetailsStandalone code itemA struct holding captured data + code logic
Associated TraitsCan implement Fn* traits if sig matchesAutomatically implements one or more Fn* traits

12.1.2 Environment Capture

Closures can use variables defined in their surrounding scope. Rust determines how to capture based on how the variable is used inside the closure body, choosing the weakest (least restrictive) mode necessary (Fn > FnMut > FnOnce; borrow > mutable borrow > move).

fn main() {
    let factor = 2; // Captured by immutable reference (&factor) for Fn
    let mut count = 0; // Captured by mutable reference (&mut count) for FnMut
    let data = vec![1, 2]; // Moved (data) into closure for FnOnce

    let multiply_by_factor = |x| x * factor; // Implements Fn, FnMut, FnOnce
    let mut increment_count = || { // Implements FnMut, FnOnce
        count += 1;
        println!("Count: {}", count);
    };
    let consume_data = || { // Implements FnOnce
        println!("Data length: {}", data.len());
        drop(data);
    };

    println!("Result: {}", multiply_by_factor(10)); // Output: Result: 20
    increment_count(); // Output: Count: 1
    increment_count(); // Output: Count: 2
    consume_data(); // Output: Data length: 2
    // consume_data(); // Error: cannot call FnOnce closure twice
    // println!("{:?}", data); // Error: data was moved

    // Borrowing rules apply: While 'increment_count' holds a mutable borrow
    // of 'count', 'count' cannot be accessed immutably or mutably elsewhere.
    // The borrow ends when 'increment_count' is no longer in use.
    println!("Final factor: {}", factor); // OK: factor was immutably borrowed
    println!("Final count: {}", count); // OK: mutable borrow ended
}

Closures capture only the data they actually need. If a closure uses a field of a struct, only that field might be captured, especially with the move keyword (see Section 12.5.2). Standard borrowing rules apply: if a closure captures a variable mutably, the original variable cannot be accessed in the enclosing scope while the closure holds the mutable borrow.

12.1.3 Closures are First-Class Citizens

Like functions, closures are first-class values in Rust: they can be assigned to variables, passed as arguments, returned from functions, and stored in data structures.

fn main() {
    // Assign closure to a variable
    let square = |x: i32| x * x;
    println!("Square of 5: {}", square(5)); // Output: Square of 5: 25

    // Pass the closure variable to an iterator adapter.
    // Since numbers.iter() yields &i32, but square expects i32,
    // we use a new closure |&x| square(x) to adapt.
    // The |&x| pattern automatically dereferences the reference from the iterator.
    let numbers = vec![1, 2, 3];
    let squares: Vec<_> = numbers.iter().map(|&x| square(x)).collect();
    println!("Squares: {:?}", squares); // Output: Squares: [1, 4, 9]
}

12.1.4 Comparison with C and C++

In C, simulating closures requires function pointers plus a void* context, demanding manual state management and lacking type safety. C++ lambdas ([capture](params){body}) are syntactically similar to Rust closures but rely on C++’s memory rules. Rust closures integrate directly with the ownership and borrowing system, ensuring memory safety at compile time.