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 assigned to variables or passed as parameters are called similar to ordinary functions, using the standard function call syntax () to enclose the potentially empty parameter list.

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. This includes passing closures to other functions, like iterator adapters. Sometimes, an intermediate closure is needed for adaptation if the closure’s signature doesn’t match what the function expects.

Example 1: Using an Adapter Closure

If you have an existing closure or want to define one with a specific signature, you might need an adapter when passing it.

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

    // Pass it to an iterator adapter.
    // The `map` adapter on numbers.iter() needs a closure compatible with &i32.
    // Since `square` expects `i32`, we define a new, inline closure `|&x| square(x)`.
    // This adapter closure takes the `&i32`, uses the `&x` pattern to get the
    // inner `i32`, and then calls the captured `square` closure with that value.
    let numbers = vec![1, 2, 3];
    let squares: Vec<_> = numbers.iter().map(|&x| square(x)).collect();
    println!("Squares (via adapter): {:?}", squares);
    // Output: Squares (via adapter): [1, 4, 9]
}

Example 2: Direct Signature Matching

Alternatively, if you know the signature required by the function you’re passing the closure to (in this case, map on an iterator yielding &i32), you can define the closure to accept that type directly. This avoids the need for an intermediate adapter closure:

fn main() {
    // Define the closure to accept the reference type directly.
    // Note: We need to dereference `x_ref` inside the closure body.
    let sqr_ref = |x_ref: &i32| (*x_ref) * (*x_ref);

    let numbers = vec![1, 2, 3];
    // Now 'sqr_ref' can be passed directly to map without an adapter.
    let squares: Vec<_> = numbers.iter().map(sqr_ref).collect();
    println!("Squares (direct): {:?}", squares); // Output: Squares (direct): [1, 4, 9]
}

Both approaches achieve the same result. Defining the closure with the expected signature, as in the second example, is often more direct when feasible. The first example demonstrates how closures can be adapted when needed, highlighting their flexibility.

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.