Iterators in Rust

About This Module

This module introduces Rust's iterator pattern, which provides a powerful and efficient way to process sequences of data. Iterators in Rust are lazy, meaning they don't do any work until you call methods that consume them. You'll learn to create custom iterators, use built-in iterator methods, and understand how iterators enable functional programming patterns while maintaining Rust's performance characteristics.

Prework

Prework Reading

Read the following sections from "The Rust Programming Language" book:

Pre-lecture Reflections

Before class, consider these questions:

  1. How do iterators in Rust differ from traditional for loops in terms of performance and safety?
  2. What does it mean for iterators to be "lazy" and why is this beneficial?
  3. How do iterator adapters (like map, filter) differ from iterator consumers (like collect, fold)?
  4. Why can't floating-point ranges be directly iterable in Rust?
  5. How does implementing the Iterator trait enable custom data structures to work with Rust's iteration ecosystem?

Learning Objectives

By the end of this module, you should be able to:

  • Create and use iterators from ranges and collections
  • Implement custom iterators by implementing the Iterator trait
  • Apply iterator adapters (map, filter, take, cycle) to transform data
  • Use iterator consumers (collect, fold, reduce, any) to produce final results
  • Understand lazy evaluation in the context of Rust iterators
  • Choose between iterator-based and loop-based approaches for different scenarios

Iterators

The iterator pattern allows you to perform some task on a sequence of items in turn.

An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished.

  • provide values one by one
  • method next provides next one
  • Some(value) or None if no more available

Some ranges are iterators:

  • 1..100
  • 0..

First value has to be known (so .. and ..123 are not)

Range as an Iterator Example

fn main() {
let mut iter = 1..3; // must be mutable

println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
}

Range between floats is not iterable

  • What about a range between floats?
#![allow(unused)]
fn main() {
let mut iter = 1.0..3.0; // must be mutable
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
}
  • In Rust, ranges over floating-point numbers (f64) are not directly iterable.

  • This is because floating-point numbers have inherent precision issues that make it difficult to guarantee exact iteration steps.

Range between characters is iterable

  • But this works.
#![allow(unused)]
fn main() {
let mut iter = 'a'..'c'; // must be mutable
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
println!("{:?}", iter.next());
}

Iterator from Scratch: Implementing the Iterator Trait

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 mut fib = Fib::new();
    for _ in 0..10 {
        print!("{:?} ",fib.next().unwrap());
    }
    println!();
}

Iterator Methods and Adapters

Pay special attention to what the output is.

  • next() -> Get the next element of an iterator (None if there isn't one)
  • collect() -> Put iterator elements in collection
  • take(N) -> take first N elements of an iterator and turn them into an iterator
  • cycle() -> Turn a finite iterator into an infinite one that repeats itself
  • for_each(||, ) -> Apply a closure to each element in the iterator
  • filter(||, ) -> Create new iterator from old one for elements where closure is true
  • map(||, ) -> Create new iterator by applying closure to input iterator
  • any(||, ) -> Return true if closure is true for any element of the iterator
  • fold(a, |a, |, ) -> Initialize expression to a, execute closure on iterator and accumulate into a
  • reduce(|x, y|, ) -> Similar to fold but the initial value is the first element in the iterator
  • zip(iterator) -> Zip two iterators together to turn them into pairs

If the method returns an iterator, you have to do something with the iterator.

See Rust provided methods for the complete list.

Iterator Methods Examples

#![allow(unused)]
fn main() {
// this does nothing!
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
println!("{:?}", v1_iter);
println!("{:?}", v1_iter.next());
}

collect can be used to put elements of an iterator into a vector:

#![allow(unused)]
fn main() {
let small_numbers : Vec<_> = (1..=10).collect();
println!("{:?}", small_numbers);
}

take turns an infinite iterator into an iterator that provides at most a specific number of elements

#![allow(unused)]
fn main() {
let small_numbers : Vec<_> = (1..).take(15).collect();
println!("{:?}", small_numbers);
}

cycle creates an iterator that repeats itself forever:

#![allow(unused)]
fn main() {
let cycle : Vec<_> = (1..4).cycle().take(21).collect();
println!("{:?}", cycle);
}

Recap

  • Iterators provide values one by one via the next() method, returning Some(value) or None
  • Ranges like 1..100 and 0.. are iterators (but floating-point ranges are not)
  • Custom iterators can be created by implementing the Iterator trait with next() method
  • Lazy evaluation: Iterators don't do work until consumed
  • Adapters (like map, filter, take, cycle) transform iterators into new iterators
  • Consumers (like collect, fold, reduce, any) produce final results from iterators
  • Iterators enable functional programming patterns while maintaining Rust's performance