Closures & Iterators
Closures and iterators are powerful functional programming features in Rust that enable expressive, efficient code. Closures are anonymous functions that can capture variables from their environment, while iterators provide lazy evaluation and composable data processing.
Closures
Closures are anonymous functions that can capture values from their surrounding scope. They provide a concise way to define small functions inline.
Basic Closure Syntax
fn main() {
// Basic closure syntax
let add_one = |x| x + 1;
println!("5 + 1 = {}", add_one(5));
// Closure with multiple parameters
let add = |x, y| x + y;
println!("3 + 4 = {}", add(3, 4));
// Closure with explicit types
let multiply: fn(i32, i32) -> i32 = |x, y| x * y;
println!("3 * 4 = {}", multiply(3, 4));
// Closure with block body
let complex_calculation = |x: i32| {
let doubled = x * 2;
let squared = doubled * doubled;
squared + 1
};
println!("Complex result: {}", complex_calculation(3));
// Closure with no parameters
let get_random = || 42;
println!("Random number: {}", get_random());
}
Variable Capture
Closures can capture variables from their environment in three ways:
fn main() {
let x = 10;
let y = 20;
let mut z = 30;
// Capture by reference (immutable)
let closure_ref = || println!("x = {}, y = {}", x, y);
closure_ref();
// Capture by mutable reference
let mut closure_mut = || {
z += 1;
println!("z = {}", z);
};
closure_mut();
closure_mut();
// Capture by value (move)
let closure_move = move || {
println!("Moved values: x = {}, y = {}", x, y);
};
closure_move();
// x and y are still accessible here because they were copied
println!("Original x = {}, y = {}", x, y);
// z is still accessible because it was borrowed mutably
println!("Final z = {}", z);
}
Closure Traits
Closures implement one or more of three traits based on what they capture:
// FnOnce: Called at most once, takes ownership
fn call_once<F>(f: F) -> i32
where
F: FnOnce() -> i32,
{
f()
}
// FnMut: Can be called multiple times, can mutate captured variables
fn call_multiple_times<F>(mut f: F)
where
F: FnMut() -> i32,
{
println!("First call: {}", f());
println!("Second call: {}", f());
}
// Fn: Can be called multiple times, immutable access to captured variables
fn call_many_times<F>(f: F)
where
F: Fn() -> i32,
{
for i in 0..3 {
println!("Call {}: {}", i + 1, f());
}
}
fn main() {
let x = 5;
// Fn closure - immutable borrow
let fn_closure = || x * 2;
call_many_times(fn_closure);
let mut counter = 0;
// FnMut closure - mutable borrow
let mut fn_mut_closure = || {
counter += 1;
counter
};
call_multiple_times(&mut fn_mut_closure);
let data = vec![1, 2, 3];
// FnOnce closure - takes ownership
let fn_once_closure = move || {
data.len() as i32
};
call_once(fn_once_closure);
// data is no longer accessible here
}
Returning Closures
// Return closure using Box for dynamic dispatch
fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |y| x + y)
}
// Return closure using impl Fn for static dispatch
fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
move |y| x * y
}
// Generic function that takes a closure factory
fn create_and_use<F, C>(factory: F, value: i32) -> i32
where
F: FnOnce() -> C,
C: Fn(i32) -> i32,
{
let closure = factory();
closure(value)
}
fn main() {
let add_5 = make_adder(5);
println!("10 + 5 = {}", add_5(10));
let multiply_3 = make_multiplier(3);
println!("7 * 3 = {}", multiply_3(7));
let result = create_and_use(|| |x| x * x, 4);
println!("4^2 = {}", result);
}
Iterators
Iterators provide a powerful way to process sequences of data lazily and efficiently.
Creating Iterators
fn main() {
// From collections
let vec = vec![1, 2, 3, 4, 5];
let iter1 = vec.iter(); // Iterator over references
let iter2 = vec.into_iter(); // Iterator over owned values
// let iter3 = vec.iter_mut(); // Iterator over mutable references
// From ranges
let range_iter = 0..10;
// Manual iterator creation
let mut manual_iter = (0..5).into_iter();
println!("Manual iteration:");
while let Some(value) = manual_iter.next() {
println!(" {}", value);
}
// Iterator from function
let fibonacci = std::iter::successors(Some((0, 1)), |(a, b)| Some((*b, a + b)))
.map(|(a, _)| a);
println!("First 10 Fibonacci numbers:");
for (i, fib) in fibonacci.take(10).enumerate() {
println!(" F({}) = {}", i, fib);
}
}
Iterator Adaptors
Iterator adaptors transform one iterator into another:
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: Transform each element
let squared: Vec<i32> = numbers.iter()
.map(|x| x * x)
.collect();
println!("Squared: {:?}", squared);
// filter: Keep elements matching predicate
let even: Vec<&i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.collect();
println!("Even numbers: {:?}", even);
// enumerate: Add indices
let with_indices: Vec<(usize, &i32)> = numbers.iter()
.enumerate()
.collect();
println!("With indices: {:?}", with_indices);
// zip: Combine with another iterator
let letters = vec!['a', 'b', 'c', 'd', 'e'];
let pairs: Vec<(&i32, &char)> = numbers.iter()
.zip(letters.iter())
.collect();
println!("Pairs: {:?}", pairs);
// take: Take first n elements
let first_three: Vec<&i32> = numbers.iter()
.take(3)
.collect();
println!("First three: {:?}", first_three);
// skip: Skip first n elements
let skip_three: Vec<&i32> = numbers.iter()
.skip(3)
.collect();
println!("Skip three: {:?}", skip_three);
// chain: Combine multiple iterators
let more_numbers = vec![11, 12, 13];
let chained: Vec<i32> = numbers.iter()
.chain(more_numbers.iter())
.cloned()
.collect();
println!("Chained: {:?}", chained);
}
Consuming Adaptors
Consuming adaptors consume the iterator and produce a final result:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// collect: Consume into collection
let doubled: Vec<i32> = numbers.iter()
.map(|x| x * 2)
.collect();
println!("Doubled: {:?}", doubled);
// reduce/fold: Combine all elements
let sum = numbers.iter().sum::<i32>();
println!("Sum: {}", sum);
let product = numbers.iter().product::<i32>();
println!("Product: {}", product);
let custom_fold = numbers.iter().fold(0, |acc, x| acc + x * x);
println!("Sum of squares: {}", custom_fold);
// find: Find first matching element
let found = numbers.iter().find(|&&x| x > 3);
println!("First > 3: {:?}", found);
// any/all: Test conditions
let has_even = numbers.iter().any(|&x| x % 2 == 0);
println!("Has even: {}", has_even);
let all_positive = numbers.iter().all(|&x| x > 0);
println!("All positive: {}", all_positive);
// count: Count elements
let count = numbers.iter().count();
println!("Count: {}", count);
// nth: Get element at position
let third = numbers.iter().nth(2);
println!("Third element: {:?}", third);
// max/min
let max = numbers.iter().max();
let min = numbers.iter().min();
println!("Max: {:?}, Min: {:?}", max, min);
}
Advanced Iterator Patterns
use std::collections::HashMap;
#[derive(Debug)]
struct Person {
name: String,
age: u32,
city: String,
}
fn main() {
let people = vec![
Person { name: "Alice".to_string(), age: 30, city: "New York".to_string() },
Person { name: "Bob".to_string(), age: 25, city: "San Francisco".to_string() },
Person { name: "Charlie".to_string(), age: 35, city: "New York".to_string() },
Person { name: "Diana".to_string(), age: 28, city: "Seattle".to_string() },
];
// Group by city
let by_city: HashMap<String, Vec<&Person>> = people.iter()
.fold(HashMap::new(), |mut acc, person| {
acc.entry(person.city.clone()).or_insert_with(Vec::new).push(person);
acc
});
println!("Grouped by city:");
for (city, residents) in &by_city {
println!(" {}: {:?}", city, residents.iter().map(|p| &p.name).collect::<Vec<_>>());
}
// Find oldest person in each city
let oldest_by_city: HashMap<String, &Person> = people.iter()
.fold(HashMap::new(), |mut acc, person| {
acc.entry(person.city.clone())
.and_modify(|current| {
if person.age > current.age {
*current = person;
}
})
.or_insert(person);
acc
});
println!("\nOldest by city:");
for (city, person) in &oldest_by_city {
println!(" {}: {} (age {})", city, person.name, person.age);
}
// Complex filtering and transformation
let young_city_dwellers: Vec<String> = people.iter()
.filter(|person| person.age < 30)
.filter(|person| person.city != "Seattle")
.map(|person| format!("{} from {}", person.name, person.city))
.collect();
println!("\nYoung city dwellers (not from Seattle): {:?}", young_city_dwellers);
}
Custom Iterators
struct Counter {
current: usize,
max: usize,
}
impl Counter {
fn new(max: usize) -> Counter {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let current = self.current;
self.current += 1;
Some(current)
} else {
None
}
}
}
// Iterator that yields Fibonacci numbers
struct Fibonacci {
a: u64,
b: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { a: 0, b: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let next = self.a;
self.a = self.b;
self.b = next + self.b;
Some(next)
}
}
fn main() {
// Using custom counter
let counter = Counter::new(5);
let sum: usize = counter.sum();
println!("Sum of 0..5: {}", sum);
// Using Fibonacci iterator
let fibonacci_sum: u64 = Fibonacci::new()
.take(10)
.sum();
println!("Sum of first 10 Fibonacci numbers: {}", fibonacci_sum);
// Combining custom iterators
let combined: Vec<(usize, u64)> = Counter::new(8)
.zip(Fibonacci::new())
.collect();
println!("Counter + Fibonacci: {:?}", combined);
}
Functional Programming Patterns
Higher-Order Functions
// Function that takes a closure
fn apply_operation<F>(numbers: Vec<i32>, operation: F) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
numbers.into_iter().map(operation).collect()
}
// Function that returns a closure
fn make_transformer(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
// Function composition
fn compose<F, G, A, B, C>(f: F, g: G) -> impl Fn(A) -> C
where
F: Fn(A) -> B,
G: Fn(B) -> C,
{
move |x| g(f(x))
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Using higher-order function
let doubled = apply_operation(numbers.clone(), |x| x * 2);
println!("Doubled: {:?}", doubled);
// Using closure factory
let triple = make_transformer(3);
let tripled = apply_operation(numbers.clone(), triple);
println!("Tripled: {:?}", tripled);
// Function composition
let add_one = |x| x + 1;
let square = |x| x * x;
let add_one_then_square = compose(add_one, square);
let result: Vec<i32> = numbers.into_iter()
.map(add_one_then_square)
.collect();
println!("Add 1 then square: {:?}", result);
}
Lazy Evaluation
fn main() {
println!("Lazy evaluation example:");
// This doesn't actually compute anything yet
let lazy_computation = (0..1_000_000)
.map(|x| {
println!("Processing {}", x); // This won't print until consumed
x * x
})
.filter(|&x| x % 2 == 0)
.take(5);
println!("Iterator created, but nothing computed yet");
// Only now does the computation happen
let results: Vec<i32> = lazy_computation.collect();
println!("Results: {:?}", results);
// Infinite iterators
println!("\nInfinite iterator example:");
let infinite_squares = (0..)
.map(|x| x * x)
.filter(|&x| x % 3 == 0);
let first_10_squares_divisible_by_3: Vec<i32> = infinite_squares
.take(10)
.collect();
println!("First 10 squares divisible by 3: {:?}", first_10_squares_divisible_by_3);
}
Parallel Iteration with Rayon
// Note: This requires the `rayon` crate
// In Cargo.toml: rayon = "1.5"
/*
use rayon::prelude::*;
fn main() {
let numbers: Vec<i32> = (0..1_000_000).collect();
// Sequential iteration
let sequential_sum: i32 = numbers.iter().map(|&x| x * x).sum();
// Parallel iteration
let parallel_sum: i32 = numbers.par_iter().map(|&x| x * x).sum();
println!("Sequential sum: {}", sequential_sum);
println!("Parallel sum: {}", parallel_sum);
// Parallel filtering and mapping
let processed: Vec<i32> = numbers
.par_iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 3)
.collect();
println!("Processed {} even numbers", processed.len());
}
*/
Performance Considerations
Iterator vs For Loop
fn main() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Iterator approach (often optimizes better)
let sum_iterator: i32 = data.iter().map(|&x| x * x).sum();
// Traditional loop
let mut sum_loop = 0;
for &item in &data {
sum_loop += item * item;
}
println!("Iterator sum: {}", sum_iterator);
println!("Loop sum: {}", sum_loop);
// Benchmarking shows iterators often perform as well or better
// due to compiler optimizations
}
Zero-Cost Abstractions
// High-level iterator code
fn high_level_processing(data: &[i32]) -> Vec<i32> {
data.iter()
.filter(|&&x| x > 0)
.map(|&x| x * 2)
.filter(|&x| x < 100)
.collect()
}
// Equivalent low-level code (what compiler might generate)
fn low_level_processing(data: &[i32]) -> Vec<i32> {
let mut result = Vec::new();
for &item in data {
if item > 0 {
let doubled = item * 2;
if doubled < 100 {
result.push(doubled);
}
}
}
result
}
fn main() {
let data = vec![-5, 10, 30, 45, 60, 75, 90];
let result1 = high_level_processing(&data);
let result2 = low_level_processing(&data);
println!("High-level result: {:?}", result1);
println!("Low-level result: {:?}", result2);
assert_eq!(result1, result2);
}
Best Practices
1. Prefer Iterators Over Loops
// Good: Iterator chain
fn process_scores(scores: &[i32]) -> Vec<i32> {
scores.iter()
.filter(|&&score| score >= 60)
.map(|&score| score * 2)
.collect()
}
// Avoid: Manual loop when iterator would work
fn process_scores_manual(scores: &[i32]) -> Vec<i32> {
let mut result = Vec::new();
for &score in scores {
if score >= 60 {
result.push(score * 2);
}
}
result
}
2. Use Appropriate Closure Traits
// Use Fn when possible (most flexible)
fn apply_readonly<F>(data: &[i32], f: F) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
data.iter().map(|&x| f(x)).collect()
}
// Use FnMut when you need to mutate captured variables
fn apply_with_state<F>(data: &[i32], mut f: F) -> Vec<i32>
where
F: FnMut(i32) -> i32,
{
data.iter().map(|&x| f(x)).collect()
}
// Use FnOnce when closure consumes captured variables
fn apply_once<F>(data: &[i32], f: F) -> i32
where
F: FnOnce(&[i32]) -> i32,
{
f(data)
}
3. Leverage Lazy Evaluation
// Good: Lazy evaluation, only processes what's needed
fn find_first_large_prime(start: u64) -> Option<u64> {
(start..)
.filter(|&n| is_prime(n))
.find(|&n| n > 1000)
}
// Avoid: Eager evaluation
fn find_first_large_prime_eager(start: u64) -> Option<u64> {
let primes: Vec<u64> = (start..10000) // Arbitrary upper bound
.filter(|&n| is_prime(n))
.collect();
primes.into_iter().find(|&n| n > 1000)
}
fn is_prime(n: u64) -> bool {
if n < 2 { return false; }
if n == 2 { return true; }
if n % 2 == 0 { return false; }
let sqrt_n = (n as f64).sqrt() as u64;
(3..=sqrt_n).step_by(2).all(|i| n % i != 0)
}
4. Chain Operations Efficiently
use std::collections::HashMap;
// Good: Single iterator chain
fn analyze_text(text: &str) -> HashMap<String, usize> {
text.split_whitespace()
.map(|word| word.to_lowercase())
.filter(|word| word.len() > 3)
.fold(HashMap::new(), |mut acc, word| {
*acc.entry(word).or_insert(0) += 1;
acc
})
}
// Avoid: Multiple passes
fn analyze_text_multiple_passes(text: &str) -> HashMap<String, usize> {
let words: Vec<String> = text.split_whitespace()
.map(|word| word.to_lowercase())
.collect();
let filtered: Vec<String> = words.into_iter()
.filter(|word| word.len() > 3)
.collect();
let mut counts = HashMap::new();
for word in filtered {
*counts.entry(word).or_insert(0) += 1;
}
counts
}
5. Use collect() Judiciously
// Good: Only collect when you need the whole collection
fn process_and_collect(data: &[i32]) -> Vec<i32> {
data.iter()
.map(|&x| x * 2)
.filter(|&x| x > 10)
.collect()
}
// Good: Don't collect if you're just going to iterate again
fn process_without_collect(data: &[i32]) -> i32 {
data.iter()
.map(|&x| x * 2)
.filter(|&x| x > 10)
.sum() // Don't collect, just sum directly
}
// Avoid: Unnecessary intermediate collections
fn avoid_intermediate_collect(data: &[i32]) -> i32 {
let doubled: Vec<i32> = data.iter().map(|&x| x * 2).collect();
let filtered: Vec<i32> = doubled.iter().filter(|&&x| x > 10).cloned().collect();
filtered.iter().sum()
}
Closures and iterators are powerful tools that enable expressive, efficient functional programming in Rust. They leverage zero-cost abstractions to provide high-level expressiveness without sacrificing performance. Master these concepts to write more elegant and maintainable Rust code.