12.3 Closure Traits: FnOnce, FnMut, and Fn

12.3.1 The Three Closure Traits

In Rust, closures implement one or more of the following traits:

  • FnOnce: The closure can be called once and may consume captured variables (taking ownership).

  • FnMut: The closure can be called multiple times and may mutate captured variables.

  • Fn: The closure can be called multiple times and only immutably borrows captured variables.

Trait Hierarchy and Dual Roles

These traits serve two primary roles:

  1. Assigned to Closures: Based on how a closure captures variables from its environment, it automatically implements one or more of these traits.

  2. Used in Function Signatures: When declaring functions that accept closures as parameters, these traits specify the requirements for the closures that can be passed in.

Trait Hierarchy from the Closure's Perspective

From the perspective of what a closure can do:

  • Fn: Most restrictive. The closure can only immutably borrow captured variables and can be called multiple times.

  • FnMut: Less restrictive. The closure can mutate captured variables and can be called multiple times.

  • FnOnce: Least restrictive. The closure can consume captured variables and might only be callable once.

Trait Bounds from the Function's Perspective

When specifying trait bounds for function parameters:

  • F: FnOnce: Least restrictive. The function can accept any closure that can be called at least once, including those that consume captured variables. This includes closures that implement FnOnce, FnMut, or Fn.

  • F: FnMut: More restrictive. The function can accept closures that can be called multiple times and may mutate captured variables. This includes closures that implement FnMut or Fn.

  • F: Fn: Most restrictive. The function can accept closures that can be called multiple times and only immutably borrow captured variables. Only closures that implement Fn satisfy this bound.

Understanding the Duality

  • From the Closure's Capability Standpoint: Fn is the most restrictive trait, limiting the closure's actions on captured variables.

  • From the Function's Acceptance Standpoint: FnOnce is the least restrictive trait bound, allowing the function to accept the widest range of closures.

12.3.2 Capturing the Environment

Depending on how a closure uses variables from its environment, Rust determines which traits the closure implements.

Examples:

  1. Capturing by Immutable Borrow (Fn):

    #![allow(unused)]
    fn main() {
    let x = 10;
    let print_x = || println!("x is {}", x);
    print_x();
    print_x(); // Can be called multiple times
    }
    • print_x borrows x immutably.
    • Can be called multiple times because it does not modify or consume x.
  2. Capturing by Mutable Borrow (FnMut):

    #![allow(unused)]
    fn main() {
    let mut x = 10;
    let mut add_to_x = |y| x += y;
    add_to_x(5);
    add_to_x(2);
    println!("x is {}", x); // Output: x is 17
    }
    • add_to_x mutably borrows x.
    • Can be called multiple times, modifying x each time.
  3. Capturing by Value (FnOnce):

    #![allow(unused)]
    fn main() {
    let x = vec![1, 2, 3];
    let consume_x = || {
        drop(x); // Moves `x` into the closure
    };
    consume_x(); // `x` is moved here
    // consume_x(); // Error: cannot call `consume_x` more than once
    // println!("x is {:?}", x); // Error: `x` has been moved
    }
    • consume_x takes ownership of x by calling drop(x).
    • Since x is moved into the closure, x is no longer accessible after consume_x() is called.
    • The closure implements the FnOnce trait and can be called only once.
    • Attempting to call consume_x() a second time or accessing x after the closure results in a compile-time error.

Why Does consume_x Take Ownership of x?

  • The closure captures x by value because it needs ownership to call drop(x), which consumes x.
  • Since x is of type Vec<i32>, which does not implement the Copy trait, moving x transfers ownership.
  • After consume_x is called, x is moved into the closure and cannot be used outside.

12.3.3 The move Keyword

The move keyword forces a closure to take ownership of the variables it captures, even if the body of the closure doesn't require ownership.

Example:

#![allow(unused)]
fn main() {
let x = vec![1, 2, 3];
let consume_x = move || println!("x is {:?}", x);
consume_x();
// x can no longer be used here
// println!("{:?}", x); // Error: x has been moved
}
  • The move keyword moves x into the closure.
  • This is useful when the closure needs to outlive the current scope, such as when spawning a new thread.

12.3.4 Passing Closures as Arguments

Closures are often passed as arguments to functions, enabling higher-order functions and flexible code design.

Example: Defining a Function That Takes a Closure

Let's define a function apply_operation that takes a value and a closure, and applies the closure to the value.

#![allow(unused)]
fn main() {
fn apply_operation<F, T>(value: T, func: F) -> T
where
    F: FnOnce(T) -> T,
{
    func(value)
}
}
  • F is a generic type that implements the FnOnce(T) -> T trait, meaning it is a closure or function that takes a T and returns a T.
  • T is a generic type for the value.

Using the Function with a Closure:

fn main() {
    let value = 5;
    let double = |x| x * 2;

    let result = apply_operation(value, double);
    println!("Result: {}", result); // Output: Result: 10
}
  • We define a closure double that multiplies its input by 2.
  • We pass value and double to apply_operation, which applies the closure to the value.

12.3.5 Functions as Closure Parameters

In Rust, functions can be used in place of closures when passing them as arguments to functions that accept closures as parameters. This is possible because function pointers implement the closure traits Fn, FnMut, and FnOnce, as long as their signatures match the expected trait bounds.

Understanding Why This Works

  • Function Pointers Implement Closure Traits: Function pointers (e.g., fn() -> T) automatically implement all three closure traits: Fn, FnMut, and FnOnce.

  • Trait Bounds: When a function specifies a trait bound like F: FnOnce() -> T, it accepts any type F that can be called at least once to produce a T. This includes closures and function pointers.

Example Using a Function Instead of a Closure

Let's revisit the simplified implementation of unwrap_or_else:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T,
    {
        match self {
            Some(value) => value,
            None => f(),
        }
    }
}

Using a Closure:

fn main() {
    let config: Option<String> = None;
    let config_value = config.unwrap_or_else(|| {
        println!("Using default configuration");
        "default_config".to_string()
    });
    println!("Config: {}", config_value);
}

Using a Function:

fn default_config() -> String {
    println!("Using default configuration");
    "default_config".to_string()
}

fn main() {
    let config: Option<String> = None;

    let config_value = config.unwrap_or_else(default_config);

    println!("Config: {}", config_value);
}
  • In both examples, we handle the case where config is None by providing a default configuration.
  • In the first example, we use a closure.
  • In the second example, we pass the function default_config directly.
  • Both approaches are valid because default_config has the signature fn() -> String, which matches the trait bound F: FnOnce() -> T.

Additional Examples

Defining a Function That Accepts a Closure or Function

fn apply_operation<F, T>(value: T, func: F) -> T
where
    F: FnOnce(T) -> T,
{
    func(value)
}
fn double(x: i32) -> i32 {
    x * 2
}
fn main() {
    let result = apply_operation(5, double);
    println!("Result: {}", result); // Output: Result: 10
}
  • The function apply_operation accepts any callable func that implements FnOnce(T) -> T.
  • We define a regular function double and pass it to apply_operation.
  • Since double has the signature fn(i32) -> i32, it satisfies the trait bound and can be used interchangeably with a closure.

Constraints and Considerations

  • Functions Cannot Capture Environment Variables: Functions cannot capture variables from their surrounding environment. If you need to access variables from the calling context, you must use a closure.
  • Signature Matching: The function's signature must exactly match the expected closure signature specified by the trait bound.
  • No State Mutation in Functions: Functions cannot capture or mutate external state, unlike closures.

12.3.6 Generic Closures

Closures can be generic over types, but their usage is limited due to the way closures are implemented.

Example:

fn apply_to<T, F>(x: T, func: F) -> T
where
    F: Fn(T) -> T,
{
    func(x)
}
fn main() {
    let double = |x| x * 2;
    let result = apply_to(5, double);
    println!("Result: {}", result); // Output: Result: 10
}
  • The closure double works with any type T that supports multiplication.
  • However, closures themselves cannot have generic parameters in their definitions.

Can Closures Be Generic?

  • Closures cannot have generic parameters like functions do.
  • You can achieve similar behavior by defining a generic function or using higher-order functions that accept closures.