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));
}

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 into acc.
  • 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 of T because 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 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()
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);
}
  1. 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]
}
  1. 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 map and filter return 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.