Rust Closures and Iterators: Functional Programming in Rust

Rust Closures and Iterators: Functional Programming in Rust
Rust is primarily categorized as a Systems Programming language, often grouped with C and C++. But surprisingly, the architecture of Rust leans far more closely heavily towards functional programming languages like Haskell or Scala.
In a functional paradigm, you treat functions identically to variables. You can pass them as arguments to other functions, return them from functions, and declare anonymous functions directly inline. Additionally, you prioritize declaring what you want to do to a collection of data, rather than manually iterating through memory addresses with a for loop to tell the computer how to do it.
In this module, we dissect Closures and Iterators—two features that allow you to write hyper-expressive, high-level code that miraculously compiles down to machine code just as fast (and sometimes faster) than a hand-written manual C loop.
1. Closures: Anonymous Functions
Closures are anonymous functions that you can save in a variable or pass as arguments to other functions.
Unlike a standard fn declaration, Closures possess a unique superpower: they can "capture" variables from the scope environment where they are defined.
Syntax Configuration
Closures in Rust do not use parentheses for their parameters; they use vertical pipes | |.
// Standard Function
fn add_one_v1(x: u32) -> u32 { x + 1 }
// Closure with explicit type annotation
let add_one_v2 = |x: u32| -> u32 { x + 1 };
// Closure heavily relying on Type Inference (Most common!)
let add_one_v3 = |x| x + 1;Because closures are typically small and short-lived, the compiler is incredibly talented at automatically inferring both the parameter types and the return type.
Capturing the Environment
A standard fn function cannot access variables declared outside of its curly braces unless they are explicitly passed in as arguments. A Closure effortlessly "closes over" its environment.
fn main() {
let baseline_offset = 15;
// The closure captures `baseline_offset` simply by referencing it!
let calculate_total = |z| z + baseline_offset;
println!("Total is: {}", calculate_total(10)); // Prints 25
}2. The Three Closure Traits (Fn, FnMut, FnOnce)
Remember the Borrow Checker? Even though a closure looks magical, it must strictly obey ownership and borrowing rules when it captures variables from its environment.
Depending on how a closure interacts with the environment, the compiler automatically implements one of three specific Traits on the closure object.
FnOnce: The closure consumes the captured variables (it takes Ownership / Moves them). Because the captured memory is destroyed at the end of the closure execution, this closure can only be called exactly once.FnMut: The closure takes a Mutable Reference (&mut) to the captured variables, allowing it to modify them.Fn: The closure captures variables by Immutable Reference (&), simply reading the environment safely without altering it.
When you write a higher-order function that accepts a closure as an argument, you must bound your generic parameter to one of these three traits!
// `<T>` is bound to the `Fn` trait.
// It requires a closure taking an i32 and returning an i32.
fn trigger_action<T>(closure: T)
where T: Fn(i32) -> i32
{
println!("Execution result: {}", closure(5));
}3. The Iterator Trait
In Rust, Iterators are lazily evaluated. This means creating an iterator does absolutely nothing. The machine code doesn't execute, and memory isn't parsed until you specifically tell the iterator to begin churning through data.
Iterators are standardized by the core Iterator trait, which realistically only strictly requires the implementation of one method: next.
pub trait Iterator {
type Item; // We will explore associated types later
// Returns `Some(Value)` if data exists, and `None` if the iteration is finished.
fn next(&mut self) -> Option<Self::Item>;
}When you write a for element in vector loop, Rust is actually internally calling .next() repeatedly behind the scenes until it encounters a None result.
Creating Iterators
You can create an iterator out of any standard Rust collection.
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter(); // Immutable Iterator (Yields &T)
let v1_mut = v1.iter_mut(); // Mutable Iterator (Yields &mut T)
let v1_own = v1.into_iter();// Consuming Iterator (Yields T, Moves ownership!)4. Chaining Methods: Map, Filter, Collect
Because iterators are incredibly standardized, Rust provides dozens of default methods on the Iterator trait called "Iterator Adaptors."
Iterator adaptors take an iterator, run a Closure on every element, and output a new Iterator. Because they are lazy, you can chain ten of them together, and Rust won't evaluate anything until a "Consuming Adaptor" is called at the end.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7];
// Chaining adaptors!
let processed_vector: Vec<i32> = numbers.into_iter()
.filter(|x| x % 2 == 0) // Keeps only the Even numbers (2, 4, 6)
.map(|x| x * 10) // Multiplies them by ten (20, 40, 60)
.collect(); // CONSUMING ADAPTOR! Triggers the iteration
println!("{:?}", processed_vector); // [20, 40, 60]
}The .collect() method is a Consuming Adaptor. It forces the lazy chain to wake up, execute the logic sequentially on the CPU, and pack the resulting data into a newly allocated Vec<i32> (or any other collection you specify).
Zero-Cost Abstractions
You might assume that creating multiple iterator adaptors and chaining them slows down your software compared to a raw #define C-loop using an index.
This is incredibly false. The Rust compiler implements "Zero-Cost Abstractions".
When LLVM compiles iterator chains, it completely unwinds them. It removes the abstraction and generates raw assembly machine-code that is often significantly faster than a raw C-loop because the compiler can definitively prove that Iterator bounds are mathematically safe, actively eliminating bounds-checking instructions (if index > length) inside the generated loop array!
Summary and Next Steps
The integration of Closures and Iterators provides Rust with a profound paradigm multiplier. You can write highly readable, declarative business logic using map and filter constructs exactly as you would in Javascript or Python, while simultaneously executing at speeds historically reserved for hand-written Assembly.
However, as we push further into systems architecture, we quickly realize that we cannot simply "borrow" every piece of data. Some data needs to be genuinely shared between completely different components, or wrapped flexibly behind abstract interfaces on the heap.
To accomplish this outside the strict bounds of the stack, we must leverage specialized structs. In the next module, we investigate Smart Pointers and internal mutability.
Read next: Rust Smart Pointers: Box, Rc, RefCell, and Arc Explained →
Quick Knowledge Check
Why does chaining Iterator adaptors (like .map().filter()) without a Consuming Adaptor (like .collect()) result in no actual code being executed?
- Because Iterators in Rust are completely decoupled from runtime evaluation; they compile strictly into meta-data until a 'for' loop invokes them.
- Because Rust Iterators are 'Lazily Evaluated'. The chain simply generates abstract logic nodes and waits for a consuming method to trigger the physical CPU execution. ✓
- Because .map() and .filter() are deprecated in Rust 2026, replaced entirely by native inline macro evaluation blocks.
- The compiler executes the adaptors normally, but explicitly dumps the data from RAM unless .collect() is called to instruct a console visualization.
Explanation: Rust iterators are strictly Lazy. Creating an iterator and calling 15 mapping functions over it evaluates to exactly zero machine instructions until a consuming adaptor (like sum() or collect()) pulls the pin, triggering the data propagation.
