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)); }
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); }
Functional Programming Classic: fold
fold(init, |acc, x| f(acc, x) )-> Initialize expression toinit, execute closure on iterator and accumulate intoacc.
iterator.fold(init, |acc, x|, f(x)) equivalent to
let mut accumulator = init;
while let Some(x) = iterator.next() {
accumulator = f(accumulator,x);
}
println!("{:?}", accumulator);
Example: compute
#![allow(unused)] fn main() { let sum_of_squares: i32 = (1..=10).fold(0,|a,x| a + x * x); println!("{}", sum_of_squares); }
#![allow(unused)] fn main() { // Another approach: using `sum` (which can be implemented using `map`) let sum_of_squares: i32 = (1..=10).map(|x| x * x).sum(); println!("{}", sum_of_squares); }
Functional Programming Classic: reduce
reduce(|x, y|, )-> Similar to fold but the initial value is the first element in the iterator
iterator.reduce(f) equivalent to
if let Some(x) = iterator.next() {
let mut accumulator = x;
while let Some(y) = iterator.next() { accumulator = f(accumulator,y}
Some(accumulator)
} else {
None
}
Differences from fold:
- no default value for an empty sequence
- output must be the same type as elements of input sequence
- output for length–one sequence equals the only element in the sequence
Example: computing the maximum number in {x^2 mod 7853: x∈[1,...,123]}, 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()
-
Using the Fibonacci iterator from earlier, collect the first 15 Fibonacci numbers into a vector and print them.
-
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
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); }
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
Task 1: 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); }
Task 2: 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] }
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
#![allow(unused)] fn main() { let result: Vec<_> = (0..5).map(|x| x * 2).collect(); println!("{:?}", result); // Output: [0, 2, 4, 6, 8] }