Functional programming in Rust, closures, iterator adaptors, and zero-cost abstractions.
Closures are anonymous functions that can capture variables from their enclosing scope. They use a compact |params| body syntax and can infer parameter and return types from context.
// Basic closure syntax
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 6
// Closure with explicit types
let add = |x: i32, y: i32| -> i32 { x + y };
println!("{}", add(2, 3)); // 5
// Capturing by reference (immutable)
let x = 5;
let add_x = |y| x + y; // Borrows x immutably
println!("{}", add_x(3)); // 8
println!("x is still: {}", x); // x is still valid
// Capturing by mutable reference
let mut counter = 0;
let mut increment = || {
counter += 1;
counter
};
println!("{}", increment()); // 1
println!("{}", increment()); // 2
// Taking ownership (move)
let s = String::from("hello");
let take_ownership = move || println!("String: {}", s);
// println!("{}", s); // Error: s moved into closure
take_ownership();
// Complex capturing example
let x = vec![1, 2, 3];
let y = vec![4, 5, 6];
let use_x = || println!("{:?}", x); // Borrows x immutably
let use_y = move || println!("{:?}", y); // Moves y
use_x();
use_y();
// Returning closures
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
let num = 5;
Box::new(move |x| x + num)
let f = returns_closure();
println!("{}", f(10)); // 15
Closures capture variables in the least restrictive way possible: immutable borrow first, then mutable borrow, and finally move (ownership). Use the move keyword to force ownership transfer, which is essential when returning closures or sending them to other threads.
Every closure in Rust implements one or more of three traits, depending on how it captures variables: FnOnce (consumes captured values), FnMut (mutates captured values), and Fn (borrows immutably).
// FnOnce: consumes captured values (can be called only once)
let s = String::from("hello");
let fn_once = move || println!("String: {}", s);
fn_once(); // Takes ownership of s
// fn_once(); // Error: already called
// FnMut: can mutate captured values
let mut counter = 0;
let mut fn_mut = || {
counter += 1;
counter
};
println!("{}", fn_mut()); // 1
println!("{}", fn_mut()); // 2
println!("Counter: {}", counter); // 2
// Fn: cannot mutate, borrows immutably
let x = 5;
let fn_trait = || println!("x: {}", x);
fn_trait();
fn_trait();
println!("x: {}", x); // Still accessible
// Using function traits as bounds
fn apply_once<F>(f: F)
where
F: FnOnce(),
{
f();
fn apply_twice<F>(mut f: F)
where
F: FnMut(),
{
f();
f();
fn apply_thrice<F>(f: F)
where
F: Fn(),
{
f();
f();
f();
let s = String::from("move me");
apply_once(move || println!("{}", s));
let mut n = 0;
apply_twice(|| n += 1);
println!("n: {}", n); // 2
apply_thrice(|| println!("Hello"));
// Real-world example: event handler
struct Button {
click_handlers: Vec<Box<dyn Fn()>>,
impl Button {
fn new() -> Self {
Button { click_handlers: Vec::new() }
}
fn on_click<F>(&mut self, handler: F)
where
F: Fn() + 'static,
{
self.click_handlers.push(Box::new(handler));
}
fn click(&self) {
for handler in &self.click_handlers {
handler();
}
}
let mut button = Button::new();
button.on_click(|| println!("Button clicked!"));
button.on_click(|| println!("Second handler!"));
button.click();
Every Fn closure also implements FnMut and FnOnce. Every FnMut closure also implements FnOnce. When accepting closures, use the least restrictive trait that works: prefer Fn when possible, then FnMut, and only use FnOnce when the closure must consume values.
The Iterator trait requires implementing just one method: next(). From that single method, you gain access to dozens of powerful adaptor methods like map, filter, fold, and more.
// Iterator trait: implements next()
let v = vec![1, 2, 3];
let mut iter = v.iter();
println!("{:?}", iter.next()); // Some(&1)
println!("{:?}", iter.next()); // Some(&2)
println!("{:?}", iter.next()); // Some(&3)
println!("{:?}", iter.next()); // None
// map() - transforms elements
let v = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// filter() - keeps matching elements
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter().filter(|x| *x % 2 == 0).collect();
// Chaining adaptors
let result: Vec<i32> = vec![1, 2, 3, 4, 5]
.iter()
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.collect();
println!("{:?}", result); // [4, 16]
// zip() - combines iterators
let a = vec![1, 2, 3];
let b = vec!["a", "b", "c"];
let zipped: Vec<(i32, &str)> = a.iter()
.cloned()
.zip(b.iter().cloned())
.collect();
// chain() - combines sequences
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let combined: Vec<i32> = a.iter().chain(b.iter()).cloned().collect();
// enumerate() - adds indices
for (idx, val) in vec!["a", "b", "c"].iter().enumerate() {
println!("{}: {}", idx, val);
// take() and skip()
let numbers = vec![1, 2, 3, 4, 5];
let first_three: Vec<i32> = numbers.iter().take(3).cloned().collect();
let skip_two: Vec<i32> = numbers.iter().skip(2).cloned().collect();
// rev() - reverse iterator
let numbers = vec![1, 2, 3];
for n in numbers.iter().rev() {
println!("{}", n); // 3, 2, 1
}
// fold() - accumulate values
let numbers = vec![1, 2, 3, 4];
let sum = numbers.iter().fold(0, |acc, x| acc + x);
println!("{}", sum); // 10
// sum() and product()
let numbers = vec![1, 2, 3, 4];
let sum: i32 = numbers.iter().sum();
let product: i32 = numbers.iter().product();
// any() and all()
let numbers = vec![1, 2, 3, 4, 5];
let has_even = numbers.iter().any(|x| x % 2 == 0);
let all_positive = numbers.iter().all(|x| x > &0);
// find() - returns first matching element
let numbers = vec![1, 2, 3, 4, 5];
let found = numbers.iter().find(|x| *x > 3);
println!("{:?}", found); // Some(&4)
Iterator adaptors like map and filter are lazy, they do nothing until a consuming method like collect(), sum(), or for_each() drives the iteration. This means you can chain many adaptors without creating intermediate collections.
Any type can become an iterator by implementing the Iterator trait with its single required method: next(). This unlocks all built-in adaptor methods automatically.
// Custom iterator: count up to n
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> {
self.count += 1;
if self.count <= self.max {
Some(self.count)
} else {
None
}
}
let counter = Counter::new(5);
for value in counter {
println!("{}", value); // 1, 2, 3, 4, 5
}
// Custom iterator: range
struct MyRange {
start: i32,
end: i32,
impl Iterator for MyRange {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.start < self.end {
let value = self.start;
self.start += 1;
Some(value)
} else {
None
}
}
let range = MyRange { start: 1, end: 5 };
let values: Vec<i32> = range.collect();
// Custom iterator: pairs from vec
struct Pairs<'a> {
items: &'a [i32],
index: usize,
impl<'a> Iterator for Pairs<'a> {
type Item = (i32, i32);
fn next(&mut self) -> Option<Self::Item> {
if self.index + 1 < self.items.len() {
let pair = (self.items[self.index], self.items[self.index + 1]);
self.index += 2;
Some(pair)
} else {
None
}
}
let nums = vec![1, 2, 3, 4, 5];
let pairs = Pairs { items: &nums, index: 0 };
for (a, b) in pairs {
println!("{}, {}", a, b);
}
// Double-ended iterator (DoubleEndedIterator)
struct BiCounter {
start: u32,
end: u32,
impl Iterator for BiCounter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.start <= self.end {
let val = self.start;
self.start += 1;
Some(val)
} else {
None
}
}
impl DoubleEndedIterator for BiCounter {
fn next_back(&mut self) -> Option<Self::Item> {
if self.start <= self.end {
let val = self.end;
self.end -= 1;
Some(val)
} else {
None
}
}
let bi = BiCounter { start: 1, end: 5 };
let values: Vec<u32> = bi.collect();
println!("{:?}", values); // [1, 2, 3, 4, 5]
Implementing DoubleEndedIterator adds a next_back() method, enabling rev() to iterate from the end. This is useful for bidirectional traversal without collecting into a Vec first.
Rust iterators are a zero-cost abstraction. The compiler optimizes iterator chains into the same machine code as hand-written loops, so you get the expressiveness of functional style with no runtime penalty.
// Zero-cost abstraction: iterators compile to same code as loops
// Method 1: For loop
let v = vec![1, 2, 3, 4, 5];
let mut sum = 0;
for i in v {
sum += i;
// Method 2: Iterator
let sum: i32 = v.iter().sum();
// Both produce identical machine code!
// Closure example: both compile to same code
let numbers = vec![1, 2, 3, 4, 5];
// Using iterator adaptor
let result1: Vec<i32> = numbers.iter()
.map(|x| x * 2)
.filter(|x| x > &4)
.collect();
// Using explicit loop
let mut result2 = Vec::new();
for x in &numbers {
let doubled = x * 2;
if doubled > 4 {
result2.push(doubled);
}
assert_eq!(result1, result2);
// Example: data processing pipeline
struct User {
id: u32,
name: String,
age: u32,
let users = vec![
User { id: 1, name: String::from("Alice"), age: 30 },
User { id: 2, name: String::from("Bob"), age: 25 },
User { id: 3, name: String::from("Charlie"), age: 35 },
];
// Iterator approach: clear and efficient
let adult_names: Vec<String> = users.iter()
.filter(|u| u.age > 30)
.map(|u| u.name.clone())
.collect();
// Real-world processing pipeline
fn process_data(numbers: Vec<i32>) -> i32 {
numbers.iter()
.filter(|x| *x % 2 == 0) // Keep evens
.map(|x| x * x) // Square them
.take(3) // Take first 3
.sum() // Sum them
println!("{}", process_data(vec![1, 2, 3, 4, 5, 6])); // 4 + 16 + 36 = 56
// Lazy evaluation advantage
let v = vec![1, 2, 3, 4, 5];
let iter = v.iter()
.map(|x| {
println!("Mapping {}", x);
x * 2
});
// Nothing printed yet - iterator is lazy!
let collected: Vec<i32> = iter.collect();
// Now "Mapping 1 2 3 4 5" is printed
Rust's iterator chains are optimized away at compile time through monomorphization and inlining. The resulting machine code is identical to hand-written loops. You never pay a runtime cost for the functional style.
Combining closures and iterators enables expressive data processing pipelines. Here is a complete example showing multiple pipeline patterns on a collection of records.
struct Record {
id: u32,
name: String,
value: f64,
category: String,
impl Record {
fn new(id: u32, name: &str, value: f64, category: &str) -> Self {
Record {
id,
name: String::from(name),
value,
category: String::from(category),
}
}
fn main() {
let records = vec![
Record::new(1, "Item A", 100.5, "Electronics"),
Record::new(2, "Item B", 50.0, "Books"),
Record::new(3, "Item C", 200.0, "Electronics"),
Record::new(4, "Item D", 75.5, "Clothes"),
Record::new(5, "Item E", 150.0, "Electronics"),
];
// Pipeline 1: Get high-value items
let high_value: Vec<&Record> = records.iter()
.filter(|r| r.value > 100.0)
.collect();
// Pipeline 2: Group by category and sum values
use std::collections::HashMap;
let by_category: HashMap<String, f64> = records.iter()
.fold(HashMap::new(), |mut map, r| {
*map.entry(r.category.clone()).or_insert(0.0) += r.value;
map
});
// Pipeline 3: Electronics total using filter + map + fold
let electronics_total: f64 = records.iter()
.filter(|r| r.category == "Electronics")
.map(|r| r.value)
.fold(0.0, |acc, v| acc + v);
println!("Electronics total: {}", electronics_total);
// Pipeline 4: Create formatted report
let report: String = records.iter()
.enumerate()
.map(|(idx, r)| format!("{}. {} - ${:.2}", idx + 1, r.name, r.value))
.collect::<Vec<String>>()
.join("\n");
println!("{}", report);
}
Use fold for aggregations like grouping, filter + map + collect for transformations, and enumerate when you need indices. The turbofish syntax collect::<Vec<String>>() tells Rust what type to collect into when it cannot be inferred.