Closures in Rust

Closures are anonymous functions you can save in a variable or pass as arguments to other functions. Unlike regular functions, closures can capture values from the scope in which they are defined. This makes them incredibly powerful and flexible, especially when working with iterators.

Basic Syntax

A closure’s syntax is lightweight. It uses pipes (|) to enclose parameters, followed by the expression that forms the body of the closure.

fn main() {
    // A simple closure that takes no arguments and prints a message.
    let say_hello = || println!("Hello, closure!");
    say_hello();

    // A closure that takes one argument and returns a value.
    let add_one = |x: u32| -> u32 {
        x + 1
    };
    println!("3 + 1 = {}", add_one(3));
}

Rust’s compiler can usually infer the types of the parameters and the return value, so you often don’t need to annotate them.

let add_one = |x| x + 1; // Types are inferred

Capturing the Environment

The defining feature of closures is their ability to “capture” variables from their enclosing scope. Closures can capture variables in three ways, which correspond to three Fn traits:

  1. Fn: Borrows values immutably (&T).
  2. FnMut: Borrows values mutably (&mut T).
  3. FnOnce: Takes ownership of values (T).

The compiler automatically determines which trait to use based on how the closure uses the captured variables.

Example: Immutable Borrow (Fn)

This closure only reads the name variable, so it borrows it immutably.

fn main() {
    let name = String::from("Alice");
    let greet = || println!("Hello, {}!", name);
    
    greet();
    greet(); // Can be called multiple times
    println!("The name '{}' is still owned by main.", name); // name is still valid here
}

Example: Mutable Borrow (FnMut)

This closure modifies the count variable, so it borrows it mutably.

fn main() {
    let mut count = 0;
    let mut increment = || {
        count += 1;
        println!("Count is now: {}", count);
    };

    increment();
    increment(); // Can be called multiple times
}

Example: Taking Ownership (FnOnce)

This closure consumes the items vector by moving it, so it can only be called once.

fn main() {
    let items = vec![1, 2, 3];
    
    let consume_and_print = || {
        // `into_iter()` takes ownership of `items`.
        for item in items.into_iter() {
            println!("{}", item);
        }
    };

    consume_and_print();
    // consume_and_print(); // Error! `items` has been moved.
}

The move Keyword

You can force a closure to take ownership of its captured variables by using the move keyword. This is particularly useful when passing a closure to a new thread.

use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    // `move` forces the closure to take ownership of `data`.
    let handle = thread::spawn(move || {
        println!("Here's the data from the new thread: {:?}", data);
    });

    // `data` is no longer accessible in the main thread because it was moved.
    // println!("{:?}", data); // This would cause a compile error.

    handle.join().unwrap();
}

Closures as Function Arguments

Closures are frequently used as arguments to higher-order functions, such as the methods on the Iterator trait.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // Use a closure with `map` to transform each element.
    let doubled: Vec<_> = numbers.iter().map(|&n| n * 2).collect();
    println!("Doubled: {:?}", doubled); // Output: [2, 4, 6, 8, 10]

    // Use a closure with `filter` to select elements.
    let evens: Vec<_> = numbers.iter().filter(|&&n| n % 2 == 0).collect();
    println!("Evens: {:?}", evens); // Output: [2, 4]
}

Summary

  • Anonymous Functions: Closures are functions without a name.
  • Environment Capture: They can capture variables from their scope via immutable borrow (Fn), mutable borrow (FnMut), or by taking ownership (FnOnce).
  • Flexibility: Their ability to capture their environment makes them extremely useful for functional patterns, especially with iterators.
  • move Keyword: Use move to force a closure to take ownership, which is essential for concurrency.