Iterators + Closures: Functional Programming in Rust
About This Module
This module explores the powerful combination of iterators and closures in Rust, which enables elegant functional programming patterns. You'll learn how to chain iterator methods with closures to create expressive, efficient data processing pipelines. This combination allows you to write concise code for complex operations like filtering, mapping, reducing, and combining data sequences while maintaining Rust's performance guarantees.
Prework
Prework Reading
Read the following sections from "The Rust Programming Language" book:
- Chapter 13.2: Processing a Series of Items with Iterators - Focus on iterator methods with closures
- Review Chapter 13.1: Closures for closure capture patterns
- Iterator documentation - Browse common methods like map, filter, fold
Pre-lecture Reflections
Before class, consider these questions:
- How do closures enable powerful iterator chaining patterns that would be difficult with function pointers?
- What are the performance implications of chaining multiple iterator adapters together?
- How does the combination of map and reduce/fold relate to the MapReduce paradigm in distributed computing?
- When would you choose fold vs reduce for aggregation operations?
- How does Rust's type system help prevent common errors in functional programming patterns?
Learning Objectives
By the end of this module, you should be able to:
- Combine iterators with closures for concise data processing
- Use functional programming patterns like map, filter, and fold effectively
- Implement complex algorithms using iterator method chaining
- Choose appropriate aggregation methods (fold, reduce, sum) for different scenarios
- Apply zip to combine multiple data sequences
- Build efficient data processing pipelines using lazy evaluation
Iterator + Closure Magic
- Operate on entire sequence, sometimes lazily by creating a new iterator
- Allows for concise expression of many concepts
for_each applies a function to each element
#![allow(unused)] fn main() { let x = (0..5).for_each(|x| println!("{}",x)); }
It is a terminal operation that returns () so you can't put another iterator after it.
filter creates a new iterator that has elements for which the given function is true
#![allow(unused)] fn main() { let not_divisible_by_3 : Vec<_> = (0..10).filter(|x| x % 3 != 0).collect(); println!("{:?}", not_divisible_by_3); }
More Iterator Operations with Closures
- Operate on entire sequence, sometimes lazily by creating a new iterator
- Allows for concise expression of many concepts
map creates a new iterator in which values are processed by a function
struct Fib { current: u128, next: u128, } impl Fib { fn new() -> Fib { Fib{current: 0, next: 1} } } impl Iterator for Fib { type Item = u128; // Calculate the next number in the Fibonacci sequence fn next(&mut self) -> Option<Self::Item> { let now = self.current; self.current = self.next; self.next = now + self.current; Some(now) } } fn main() { let fibonacci_squared : Vec<_> = Fib::new().take(10).map(|x| x*x).collect(); println!("{:?}", fibonacci_squared); }
Calculate Primes with .any()
any is true if the passed function is true on some element
Is a number prime?
fn is_prime(k:u32) -> bool { !(2..k).any(|x| k % x == 0) } fn main() { println!("{}", is_prime(33)); println!("{}", is_prime(31)); }
Create infinite iterator over primes:
#![allow(unused)] fn main() { // create a new iterator let primes = (2..).filter(|k| !(2..*k).any(|x| k % x == 0)); let v : Vec<_> = primes.take(20).collect(); println!("{:?}", v); }
Maybe unnecessarily complex? A closure in a closure!
Functional Programming Classic: fold
fold(init, |acc, x| f(acc, x) ) -> acc- Initialize expression to
init, execute closure on iterator and accumulate intoacc. - If the iterator is empty, simply return
init. - If the iterator is not empty, return the accumulated value.
It is equivalent to the following explicit implementation:
// Explicit implementation of `fold`
fn fold(iterator: Iterator<T>, init: T, f: |T, T| -> T) -> T {
// initialize accumulator to init
let mut accumulator = init;
// iterate over the iterator and put next element into x
while let Some(x) = iterator.next() {
// execute closure on iterator and accumulate into `acc`
accumulator = f(accumulator, x);
}
// return the accumulated value
return accumulator;
}
Example: compute
#![allow(unused)] fn main() { let sum_of_squares: i32 = (1..=10).fold(5, |a,x| a + x * x); println!("{}", sum_of_squares); }
fold example
Or let's say we want to compute the final position after a series of moves.
// Move directions on a 2D grid enum Direction { North, South, East, West, } fn main() { let moves = vec![Direction::North, Direction::East, Direction::North, Direction::West]; // Starting position (x, y) = (3, 4) let final_position = moves.iter().fold((3, 4), |(x, y), dir| { match dir { Direction::North => (x, y + 1), // move north (increase y) Direction::South => (x, y - 1), // move south (decrease y) Direction::East => (x + 1, y), // move east (increase x) Direction::West => (x - 1, y), // move west (decrease x) } }); println!("Final position: {:?}", final_position); }
Functional Programming Classic: reduce
reduce(|x, y|, ) -> Option<T>- Similar to fold but the initial value is the first element in the iterator
- No default value for an empty sequence
- Returns
Option<T>instead ofTbecause the iterator may be empty
iterator.reduce(f) equivalent to
// Explicit implementation of `reduce`
fn reduce(iterator: Iterator<T>, f: |T, T| -> T) -> Option<T> {
// get the first element in the iterator
if let Some(x) = iterator.next() {
let mut accumulator = x; // initialize accumulator to the first element
while let Some(y) = iterator.next() { accumulator = f(accumulator,y)}
Some(accumulator) // return the accumulated value in Some() variant
} else {
None // return None if the iterator is empty
}
}
Example: Find the maximum number in a sequence.
#![allow(unused)] fn main() { let x = vec![33, 21, 2, 88, 42]; let x = x.into_iter().reduce(|x,y| x.max(y)).unwrap(); println!("{}", x); }
reduce example
Example: Find the longest word in a sequence.
fn main() { let words = vec!["apple", "strawberry", "pear", "banana"]; // reduce returns an Option because the vector might be empty let longest_word = words.iter().reduce(|longest, current| { if current.len() > longest.len() { current } else { longest } }); match longest_word { Some(word) => println!("The longest word is: {}", word), None => println!("The list was empty!"), } }
Another reduce example
Example: computing the maximum number in , i.e. finds the largest squared value (modulo 7853) across all integers from 1 to 123.
#![allow(unused)] fn main() { let x = (1..=123).map(|x| (x*x) % 7853).reduce(|x,y| x.max(y)).unwrap(); println!("{}", x); }
where y is the next element in the iterator.
#![allow(unused)] fn main() { // in this case one can use the builtin `max` method (which can be implemented, using `fold`) let x = (1..=123).map(|x| (x*x) % 7853).max().unwrap(); println!("{}", x); }
Combining Two Iterators: zip
- Returns an iterator of pairs
- The length is the minimum of the lengths
#![allow(unused)] fn main() { let v: Vec<_>= (1..10).zip(11..20).collect(); println!("{:?}", v); }
Inner product of two vectors:
#![allow(unused)] fn main() { let x: Vec<f64> = vec![1.1, 2.2, -1.3, 2.2]; let y: Vec<f64> = vec![2.7, -1.2, -1.1, -3.4]; let inner_product: f64 = x.iter().zip(y.iter()).map(|(a,b)| a * b).sum(); println!("{}", inner_product); }
Recap
for_each- apply function to each elementfilter- create iterator with elements matching a conditionmap- transform elements into new valuesany- test if any element satisfies a conditionfold- accumulate with explicit initial valuereduce- accumulate using first element (returnsOption)zip- combine two iterators into pairs
In-Class Exercise
Time: 5 minutes
Complete the following tasks using iterators and their methods:
- Create a vector containing the first 10 odd numbers (1, 3, 5, ..., 19)
- Use a range starting from 1
- Use iterator adapters and
collect()
Task 1 Solution: First 10 odd numbers
#![allow(unused)] fn main() { let odd_numbers: Vec<_> = (1..).step_by(2).take(10).collect(); println!("{:?}", odd_numbers); // Output: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] }
Alternative solution using filter:
#![allow(unused)] fn main() { let odd_numbers: Vec<_> = (1..20).filter(|x| x % 2 == 1).collect(); println!("{:?}", odd_numbers); }
- Using the Fibonacci iterator from earlier, collect the first 15 Fibonacci numbers into a vector and print them.
Task 2 Solution: First 15 Fibonacci numbers
struct Fib { current: u128, next: u128, } impl Fib { fn new() -> Fib { Fib{current: 0, next: 1} } } impl Iterator for Fib { type Item = u128; // Calculate the next number in the Fibonacci sequence fn next(&mut self) -> Option<Self::Item> { let now = self.current; self.current = self.next; self.next = now + self.current; Some(now) } } fn main() { let fib_numbers: Vec<_> = Fib::new().take(15).collect(); println!("{:?}", fib_numbers); // Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377] }
- Create an iterator that:
- Starts with the range 1..=20
- Filters to keep only numbers divisible by 3
- Multiplies each remaining number by 2
- Collects into a vector
Task 3: Filter and map
#![allow(unused)] fn main() { let result: Vec<_> = (1..=20) .filter(|x| x % 3 == 0) .map(|x| x * 2) .collect(); println!("{:?}", result); // Output: [6, 12, 18, 24, 30, 36] }
Bonus Challenge: Without running the code, predict what this will output:
#![allow(unused)] fn main() { let result: Vec<_> = (0..5).map(|x| x * 2).collect(); println!("{:?}", result); }
Bonus Challenge Solution
#![allow(unused)] fn main() { let result: Vec<_> = (0..5).map(|x| x * 2).collect(); println!("{:?}", result); // Output: [0, 2, 4, 6, 8] }
Solution Discussion
After attempting the exercise, compare your solutions with a neighbor. Key concepts to verify:
- Did you chain iterator adapters before calling a consumer?
- Did you understand that
mapandfilterreturn iterators, not final values? - Did you remember that iterators are lazy and need a consumer to produce results?
Solutions will be posted after the lecture.