Iterators

The Iterator Model

The iterator model in Rust is a pull-based sequence abstraction in which a consumer repeatedly requests the next element from a pipeline of composable state machines, separating how elements are produced, how they are transformed, and how they are consumed.

The Three Stages of an Iterator Pipeline

Every iterator pipeline consists of three stages:

+--------------------+     +------------------------+     +--------------------+
|     1. Source      |     |   2. Adapters (Lazy)   |     | 3. Consumer (Eager)|
|  vec.iter()        | ──> |  .filter(...)          | ──> |  .sum()            |
|  (0..100)          |     |  .map(...)             |     |  .collect()        |
|  file.lines()      |     |  .take(5)              |     |  for x in ...      |
+--------------------+     +------------------------+     +--------------------+
  1. Source (Producer): Creates the initial iterator over a collection, range, I/O stream, or generator state machine.
  2. Adapters (Transformers): Wrap an existing iterator in a new struct (Filter, Map, Take) that transforms or filters items on demand without allocating intermediate collections.
  3. Consumer (Terminal Operation): Drives the pipeline by calling .next() in a loop until the iterator returns None or the consumer short-circuits.

Imperative Loop vs. Declarative Iterator Pipeline

An iterator pipeline expresses data transformations declaratively while compiling down to the same machine code as a hand-written loop:

fn main() {
    let numbers = [1, 2, 3, 4, 5, 6];

    // 1. Imperative loop with manual mutable accumulator:
    let mut sum_loop = 0;
    for &n in &numbers {
        if n % 2 == 0 {
            sum_loop += n * n;
        }
    }

    // 2. Declarative iterator pipeline (Source -> Filter -> Map -> Sum):
    let sum_iter: i32 = numbers
        .iter()
        .filter(|&&n| n % 2 == 0)
        .map(|&n| n * n)
        .sum();

    assert_eq!(sum_loop, sum_iter);
    println!("Sum of even squares: {sum_iter}");
}
Sum of even squares: 56

Pull-Based Execution Flow

Because Rust iterators are pull-based, no element is produced until the consumer asks for it. On each step, the consumer calls .next() on the outermost adapter, which pulls from its inner iterator until an item satisfies all stages:

Consumer (.sum())
  │ calls .next() on Map
  ▼
Map (.map(|&n| n * n))
  │ calls .next() on Filter
  ▼
Filter (.filter(|&&n| n % 2 == 0))
  │ calls .next() on SliceIter ([1, 2, 3, ...])
  ▼
SliceIter yields Some(&1) (rejected by Filter) -> Some(&2) (accepted -> mapped to 4)

This pull model means infinite sources (such as 0.. or std::iter::repeat(1)) can be safely composed with .take(n) without hanging or exhausting memory.