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:
-
Assigned to Closures: Based on how a closure captures variables from its environment, it automatically implements one or more of these traits.
-
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 implementFnOnce
,FnMut
, orFn
. -
F: FnMut
: More restrictive. The function can accept closures that can be called multiple times and may mutate captured variables. This includes closures that implementFnMut
orFn
. -
F: Fn
: Most restrictive. The function can accept closures that can be called multiple times and only immutably borrow captured variables. Only closures that implementFn
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:
-
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
borrowsx
immutably.- Can be called multiple times because it does not modify or consume
x
.
-
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 borrowsx
.- Can be called multiple times, modifying
x
each time.
-
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 ofx
by callingdrop(x)
.- Since
x
is moved into the closure,x
is no longer accessible afterconsume_x()
is called. - The closure implements the
FnOnce
trait and can be called only once. - Attempting to call
consume_x()
a second time or accessingx
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 calldrop(x)
, which consumesx
. - Since
x
is of typeVec<i32>
, which does not implement theCopy
trait, movingx
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 movesx
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 theFnOnce(T) -> T
trait, meaning it is a closure or function that takes aT
and returns aT
.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
anddouble
toapply_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
, andFnOnce
. -
Trait Bounds: When a function specifies a trait bound like
F: FnOnce() -> T
, it accepts any typeF
that can be called at least once to produce aT
. 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
isNone
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 signaturefn() -> String
, which matches the trait boundF: 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 callablefunc
that implementsFnOnce(T) -> T
. - We define a regular function
double
and pass it toapply_operation
. - Since
double
has the signaturefn(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 typeT
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.