Fn / FnMut / FnOnce
Fn / FnMut / FnOnce
Level 6 — Closures & Functional Patterns Closure traits: borrows immutably / borrows mutably / takes ownership (consumed on call).
1. Prerequisites
- Closures (
|args| body) — The anonymous functions these traits describe. - Trait Bound — How you apply these traits to generic functions.
- Borrowing (
&) — The core mechanism that separates these three traits.
2. Term Category
Rust-specific (closure bounds): In Rust, every closure expression generates a unique, unnameable type created by the compiler. Because you cannot specify anonymous closure types directly in function signatures, Rust uses generic trait bounds parameterized by three core traits: Fn, FnMut, and FnOnce. These traits categorize how closures access their captured environment.
3. Explanation
(1) Design Motivation — "Why did we design this?"
When accepting a closure as a parameter, a higher-order function must specify what operations it will perform on the closure:
Fn(Shared Read-Only): Captures environment variables via shared references (&T). Can be invoked repeatedly and concurrently across multiple execution paths without mutating captured state.FnMut(Exclusive Mutable): Captures environment variables via mutable references (&mut T). Can be invoked repeatedly, but requires exclusive&mutaccess because it mutates internal state.FnOnce(Ownership Consumption): Takes full ownership of captured variables (T). Can be invoked only once because calling it consumes or moves captured state out of the closure body.
(2) Deep Dive — Trait Subtyping Hierarchy & Desugaring
Rust establishes an explicit trait inheritance hierarchy between closure traits:
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn<Args>: FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
- Every closure implementing
Fnautomatically implementsFnMutandFnOnce. - A function accepting
F: FnOnce()accepts any closure, whereas a function requiringF: Fn()strictly rejects closures that mutate or move their environment.
(3) Reality Metaphor
Fn(Reading a Boarding Pass): A gate attendant checks your boarding pass. They read your name as many times as needed without damaging or modifying the pass.FnMut(Stamping a Loyalty Card): A coffee shop barista stamps your loyalty card. They mutate the card state on every visit. You can return repeatedly, but the card state changes on each call.FnOnce(Redeeming a One-Time Coupon): You hand a paper gift certificate to a cashier. They scan it and tear it up. It is consumed entirely and cannot be redeemed a second time.
(4) Rust Code Examples
Categorizing Closures
fn main() {
let name = String::from("Alice");
let mut count = 0;
let data = vec![1, 2, 3];
// Implements Fn (shared borrow of `name`)
let read_name = || println!("Hello, {name}");
// Implements FnMut (mutable borrow of `count`)
let mut inc = || count += 1;
// Implements FnOnce (consumes `data` by moving it into drop)
let consume = || drop(data);
read_name();
inc();
consume();
}
Trait Bounds in Higher-Order Functions
fn repeat_read<F: Fn()>(f: F) {
f();
f();
}
fn execute_once<F: FnOnce()>(f: F) {
f();
}
fn main() {
let msg = String::from("Notification");
repeat_read(|| println!("{msg}"));
execute_once(move || println!("Final: {msg}"));
}
4. Common Mistakes & Pitfalls
Mistake 1: Requiring Fn Bound when Accepting a Closure that Mutates Captured State
The mistake: Constraining a generic parameter with F: Fn() when passing a closure that modifies environment variables (FnMut).
Why it is wrong: Fn requires shared reference &self invocation. Closures mutating state require &mut self (FnMut). Passing an FnMut closure to an Fn bound causes compiler error E0525 / E0596.
Incorrect:
fn apply_twice<F: Fn()>(f: F) { f(); f(); }
let mut count = 0;
// apply_twice(|| count += 1); // ❌ Error E0596: cannot borrow as mutable inside `Fn` closure
Fix:
fn apply_twice<F: FnMut()>(mut f: F) { f(); f(); } // Use FnMut bound!
Mistake 2: Attempting to Invoke an FnOnce Parameter Multiple Times
The mistake: Calling an F: FnOnce() parameter twice inside a higher-order function.
Why it is wrong: FnOnce consumes self on invocation. Subsequent calls attempt to execute on a moved value, triggering compiler error E0382.
Incorrect:
fn run_once<F: FnOnce()>(f: F) {
f();
// f(); // ❌ Error E0382: use of moved value `f`
}
Mistake 3: Returning a Borrowing Closure without move when Returning impl Fn()
The mistake: Returning a closure that captures stack variables without using move.
Why it is wrong: Closures borrow environment references by default. Returning the closure creates a dangling reference to local function stack variables.
5. Practice Exercises
Exercise 1: Asynchronous Event Bus Handler Registry
Scenario: Implement an event bus EventBus where listeners register callbacks for specific event topics. Support both immutable read listeners (Fn(&Event)) and stateful mutating listeners (FnMut(&Event)).
Requirements:
- Define
struct Event { pub topic: String, pub payload: String }. - Implement
EventBusstoringlisteners: Vec<Box<dyn FnMut(&Event)>>. - Add method
subscribe<F>(&mut self, f: F) where F: FnMut(&Event) + 'static. - Add method
publish(&mut self, event: &Event). - Write unit tests verifying listener invocation.
Answer
Implementation
pub struct Event {
pub topic: String,
pub payload: String,
}
pub struct EventBus {
listeners: Vec<Box<dyn FnMut(&Event)>>,
}
impl EventBus {
pub fn new() -> Self {
Self { listeners: Vec::new() }
}
pub fn subscribe<F>(&mut self, f: F)
where
F: FnMut(&Event) + 'static,
{
self.listeners.push(Box::new(f));
}
pub fn publish(&mut self, event: &Event) {
for listener in self.listeners.iter_mut() {
listener(event);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_bus() {
let mut bus = EventBus::new();
let mut received_count = 0;
// FnMut closure modifying local counter
bus.subscribe(move |evt| {
if evt.topic == "ALERT" {
received_count += 1;
}
});
let evt = Event { topic: "ALERT".into(), payload: "High Load".into() };
bus.publish(&evt);
}
}
Technical Explanation
FnMut(&Event)allows closures to mutate internal captured state (like counter variables) across calls.Box<dyn FnMut(&Event) + 'static>enables heterogeneous listener storage in a single collection.
Exercise 2: Single-Use Transaction Commit Engine (FnOnce)
Scenario: Build an atomic transaction manager execute_transaction<T, F> that takes an owned context T and a single-use closure F: FnOnce(T) -> Result<T, String>.
Requirements:
- Implement
execute_transaction<T, F>(context: T, action: F) -> Result<T, String> where F: FnOnce(T) -> Result<T, String>. - Write unit tests passing closures that consume payload ownership.
Answer
Implementation
pub fn execute_transaction<T, F>(context: T, action: F) -> Result<T, String>
where
F: FnOnce(T) -> Result<T, String>,
{
action(context)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fn_once_transaction() {
let payload = String::from("session_data");
let res = execute_transaction(payload, |mut data| {
data.push_str("_validated");
Ok(data)
});
assert_eq!(res, Ok("session_data_validated".to_string()));
}
}
Technical Explanation
FnOnce(T)takes full ownership ofcontexton call.- The closure cannot be called more than once, preserving single-use atomic transaction guarantees.
Exercise 3: High-Throughput Request Middleware Pipeline (Fn)
Scenario: Implement a HTTP request filter chain fn run_pipeline<F: Fn(&str) -> bool>(req: &str, filters: &[F]) -> bool that executes multiple read-only Fn closures over a request path.
Requirements:
- Evaluate request string slice through slice of
Fnclosures. - Return
trueif all filters pass,falseotherwise. - Write unit tests.
Answer
Implementation
pub fn run_pipeline<F: Fn(&str) -> bool>(req: &str, filters: &[F]) -> bool {
filters.iter().all(|f| f(req))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fn_pipeline() {
let f1 = |path: &str| path.starts_with("/api");
let f2 = |path: &str| !path.contains("admin");
let filters = vec![f1, f2];
assert!(run_pipeline("/api/v1/users", &filters));
assert!(!run_pipeline("/api/v1/admin", &filters));
}
}
Technical Explanation
Fn(&str) -> boolallows invoking filters immutably across iteration.filters.iter().all(...)processes closures in a short-circuiting read-only pass.
6. Related Terms
moveClosure — A keyword that forces a closure to take ownership of its environment, which heavily interacts withFnOnce.- Trait Bound — The generic mechanism you use to apply
Fn,FnMut, andFnOnceto functions. - Closures (
|args| body) — Related concept: Closure. Function Pointers(fn()) — Related concept:Function Pointers(fn()).
7. Key Takeaways
Fn: Captures by shared borrow (&T). Callable infinitely and concurrently.FnMut: Captures by mutable borrow (&mut T). Callable repeatedly with exclusive access.FnOnce: Captures by move/ownership (T). Callable only once.- Trait hierarchy:
FnimplementsFnMut, which implementsFnOnce.