Closure Architecture (Fn, FnMut, FnOnce) & Zero-Cost Iterators
Master anonymous closure structs, environment capture modes, the closure trait hierarchy (Fn, FnMut, FnOnce), and lazy zero-cost iterators.
Functional programming in Rust is not an afterthought; it is built into the language core with zero runtime overhead.
Under the hood, closures in Rust are not opaque runtime function objects or dynamic lambda dictionaries. They are anonymous structs synthesized by the compiler with explicit field layouts matching their captured environment.
Every closure implementing Fn also implements FnMut, and every FnMut closure implements FnOnce.
1. How Closures are Represented in Hardware#
When you write a closure, rustc creates an unnameable anonymous struct containing fields for every variable captured from the surrounding lexical scope:
12345678910fn main() { let factor = 10; let offset = 5; // The compiler synthesizes an anonymous struct: // struct ClosureEnvironment<'a> { factor: &'a i32, offset: &'a i32 } let calculate = |x: i32| (x * factor) + offset; println!("Result: {}", calculate(3)); // 35 }
Depending on how captured variables are manipulated within the closure body, the fields of this anonymous struct will store values by reference (&T), by mutable reference (&mut T), or by value (ownership move).
2. The Three Closure Traits#
Rust distinguishes three closure traits based on the manner in which the closure consumes its captured environment:
| Trait | Calling Signature | Capture Mechanism | Times Callable | Typical Use Case |
|---|---|---|---|---|
FnOnce | fn call_once(self, args) -> Output | By value (consumes self / moves fields). | Strictly once. | Thread initialization, teardown callbacks. |
FnMut | fn call_mut(&mut self, args) -> Output | By mutable borrow (&mut self). | Multiple times. | Accumulators, counters, event handlers. |
Fn | fn call(&self, args) -> Output | By immutable borrow (&self). | Multiple times (concurrently safe). | Pure calculations, filter predicates. |
Mathematically, the closure traits form a subset hierarchy:
If a function requests an argument implementing FnOnce, you can pass it an Fn closure, an FnMut closure, or an FnOnce closure.
move (e.g. move || println!("{}", text)) forces the closure to take ownership of captured variables by value, even if the closure body only performs read-only operations. This is essential when spawning threads (std::thread::spawn), ensuring the captured data outlives the originating stack frame.3. Zero-Cost Lazy Iterators#
Iterators in Rust are completely lazy: constructing an iterator chain allocates zero heap memory and executes zero loop iterations until a consumer method (like .collect(), .sum(), or .fold()) drives the pipeline:
12345678fn compute_sum_of_squares(numbers: &[i32]) -> i32 { // Zero allocations: compiles into a single pointer-incrementing CPU loop! numbers .iter() .filter(|&&x| x % 2 == 0) .map(|&x| x * x) .sum() }
Because iterator adapters (Map, Filter, Zip) are generic structs evaluated at compile time, LLVM inlines the entire pipeline into the exact equivalent of a handcrafted C while loop, complete with loop unrolling and SIMD auto-vectorization.
4. Concept Verification#
Test your understanding of closures and iterator pipelines:
What dictates whether a closure implements Fn, FnMut, or FnOnce in Rust?
Why are Rust iterator chains considered 'zero-cost abstractions' compared to Python map/filter generators?