Rust Functional Programming: Closures and Iterators
Master closures and iterators in Rust. Learn how zero-cost abstractions let you write expressive, high-level code without sacrificing performance.
Fourteenth post in the intermediate Rust series. After mastering smart pointers, we now turn to Rust’s functional programming features. Rust borrows heavily from functional languages: it gives you closures (anonymous functions) and iterators (lazy, composable sequences). The key promise is zero-cost abstractions — these high-level constructs compile down to the same machine code as hand-written loops.
graph TD
A["Functional Rust"] --> B["Closures<br/>Fn, FnMut, FnOnce"]
A --> C["Iterators<br/>Lazy evaluation"]
A --> D["Zero-Cost Abstractions"]
A --> E["map, filter, fold"]
Closures: Anonymous Functions#
A closure is an anonymous function that can capture its environment. Unlike regular functions, closures can “close over” variables from their surrounding scope.
Basic Syntax#
fn main() {
let add_one = |x: i32| x + 1;
println!("Result: {}", add_one(5)); // Result: 6
// Closures can capture the environment
let multiplier = 3;
let multiply = |x: i32| x * multiplier;
println!("Result: {}", multiply(5)); // Result: 15
}rustThe |x: i32| syntax defines the parameter list, similar to closures in other languages.
Capturing the Environment#
Closures capture variables from their environment in three ways:
Fn— captures by reference (&T)FnMut— captures by mutable reference (&mut T)FnOnce— captures by value (moves ownership)
fn main() {
let list = vec![1, 2, 3];
// Fn: captures by reference
let print_list = || println!("List: {:?}", list);
print_list();
// FnMut: captures by mutable reference
let mut counter = 0;
let mut increment = || {
counter += 1;
counter
};
println!("Counter: {}", increment()); // Counter: 1
println!("Counter: {}", increment()); // Counter: 2
// FnOnce: moves ownership
let owned = String::from("hello");
let consume = || println!("Consumed: {owned}");
consume();
// println!("{owned}"); // ❌ Error: value moved
}rust[!NOTE] A closure that implements
FnOncecan also be used asFnMutorFn, but not vice versa. The compiler infers the least restrictive trait needed.
Closures as Function Arguments#
Closures are most powerful when passed as arguments to higher-order functions:
fn apply_to_list<T>(list: &[T], f: impl Fn(&T)) {
for item in list {
f(item);
}
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
apply_to_list(&numbers, |x| println!("Number: {x}"));
}rustClosures as Return Values#
You can also return closures from functions using impl Fn:
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
|x| x * factor
}
fn main() {
let double = make_multiplier(2);
let triple = make_multiplier(3);
println!("Double 5: {}", double(5)); // Double 5: 10
println!("Triple 5: {}", triple(5)); // Triple 5: 15
}rustIterators: Lazy Sequences#
An iterator is a construct that produces a sequence of values. In Rust, iterators are lazy — they do nothing until you call a consuming method like collect() or for.
Creating Iterators#
Every collection in Rust has an .iter() method that returns an iterator:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// .iter() returns an iterator
let iter = numbers.iter();
// Iterators are lazy — nothing happens yet
for num in iter {
println!("{num}");
}
}rustIterator Adapters#
Iterator adapters take an iterator and return a new iterator. The most common ones are map, filter, and fold:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0) // keep even numbers
.map(|&x| x * 2) // double each
.collect(); // collect into a Vec
println!("{:?}", result); // [4, 8, 12, 16, 20]
}rustgraph LR
A["[1,2,3,4,5,6,7,8,9,10]"] --> B["filter: even"]
B --> C["[2,4,6,8,10]"]
C --> D["map: x2"]
D --> E["[4,8,12,16,20]"]
The fold Method#
fold is the functional equivalent of a for loop with an accumulator:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Sum all numbers
let sum: i32 = numbers.iter().fold(0, |acc, &x| acc + x);
println!("Sum: {sum}"); // Sum: 15
// Find the maximum
let max = numbers.iter().fold(0, |acc, &x| if x > acc { x } else { acc });
println!("Max: {max}"); // Max: 5
}rustCustom Iterators with impl Iterator#
You can create your own iterators by implementing the Iterator trait:
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Counter {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let counter = Counter::new(5);
let sum: u32 = counter.sum();
println!("Sum: {sum}"); // Sum: 15
}rustZero-Cost Abstractions#
The hallmark of Rust’s functional features is that they compile down to the same machine code as manual loops. Let’s prove it:
// Functional style
fn functional_sum(numbers: &[i32]) -> i32 {
numbers.iter().filter(|&&x| x > &0).map(|&x| x * 2).sum()
}
// Imperative style
fn imperative_sum(numbers: &[i32]) -> i32 {
let mut sum = 0;
for &x in numbers {
if x > 0 {
sum += x * 2;
}
}
sum
}rustBoth functions produce identical assembly. The compiler optimizes away the iterator chain entirely.
[!TIP] Rust’s iterators are a textbook example of zero-cost abstractions. You get the expressiveness of functional programming with the performance of hand-optimized C.
ELI5 Analogy: The Assembly Line#
Think of iterators like an assembly line in a factory:
- The raw materials (the original collection) arrive at the start.
- Each adapter (
map,filter, etc.) is a workstation that transforms or removes items. - The conveyor belt (the iterator) is lazy — items only move when a worker at the end pulls them.
- The final consumer (
collect,sum,for) is the worker who takes the finished products off the belt.
Because the belt is lazy, if the worker only needs 3 items, the factory only processes 3 — it doesn’t waste energy processing the entire batch!
Summary#
- Closures are anonymous functions that can capture their environment, with three capture modes:
Fn,FnMut, andFnOnce. - Iterators are lazy sequences that produce values on demand, with powerful adapters like
map,filter, andfold. - Zero-cost abstractions mean functional-style code compiles to the same machine code as imperative loops.
- Custom iterators can be created by implementing the
Iteratortrait.
In the next post, we will dive into Fearless Concurrency — Rust’s approach to safe parallel programming with threads, channels, and shared state.