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:

Pre-lecture Reflections

Before class, consider these questions:

  1. How do closures enable powerful iterator chaining patterns that would be difficult with function pointers?
  2. What are the performance implications of chaining multiple iterator adapters together?
  3. How does the combination of map and reduce/fold relate to the MapReduce paradigm in distributed computing?
  4. When would you choose fold vs reduce for aggregation operations?
  5. 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 to init, execute closure on iterator and accumulate into acc.

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 element
  • filter - create iterator with elements matching a condition
  • map - transform elements into new values
  • any - test if any element satisfies a condition
  • fold - accumulate with explicit initial value
  • reduce - accumulate using first element (returns Option)
  • zip - combine two iterators into pairs

In-Class Exercise

Time: 5 minutes

Complete the following tasks using iterators and their methods:

  1. Create a vector containing the first 10 odd numbers (1, 3, 5, ..., 19)

    • Use a range starting from 1
    • Use iterator adapters and collect()
  2. Using the Fibonacci iterator from earlier, collect the first 15 Fibonacci numbers into a vector and print them.

  3. 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 map and filter return 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]
}