ExamplesBy LevelBy TopicLearning Paths
405 Intermediate

405: Iterator Trait Deep Dive

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "405: Iterator Trait Deep Dive" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Rust's `Iterator` trait is one of the most carefully designed APIs in the standard library. Key difference from OCaml: 1. **One method**: Rust requires implementing only `next`; OCaml's `Seq.t` is a recursive type that requires understanding the `node` type structure.

Tutorial

The Problem

Rust's Iterator trait is one of the most carefully designed APIs in the standard library. Implementing just fn next(&mut self) -> Option<Self::Item> unlocks 75+ adapter methods: map, filter, fold, take, skip, chain, zip, enumerate, flat_map, and more — all built as default methods on top of next. This design means any custom data structure that implements Iterator gains the entire rich functional pipeline for free, with zero-cost abstraction through lazy evaluation and monomorphization.

Iterators power Rust's approach to functional programming without allocations: entire processing pipelines execute lazily, only materializing with .collect() or .fold().

🎯 Learning Outcomes

  • • Understand how implementing only fn next unlocks the entire iterator adapter library
  • • Learn how to create stateful iterators using struct fields
  • • See how size_hint enables efficient pre-allocation in .collect()
  • • Understand lazy evaluation: adapters are zero-cost until consumed by for or a terminal operation
  • • Learn how custom iterators compose with std adapters (take, zip, enumerate)
  • Code Example

    // Pipeline
    let sum: i32 = (1..=10)
        .filter(|x| x % 2 == 0)
        .map(|x| x * x)
        .sum();
    
    // flat_map
    let pairs: Vec<(i32, i32)> = (1..=3)
        .flat_map(|x| (1..=3).map(move |y| (x, y)))
        .filter(|(x, y)| x < y)
        .collect();
    
    // zip
    let names = ["Alice", "Bob", "Carol"];
    let scores = [95, 87, 91];
    let combined: Vec<_> = names.iter().zip(scores.iter()).collect();

    Key Differences

  • One method: Rust requires implementing only next; OCaml's Seq.t is a recursive type that requires understanding the node type structure.
  • Mutability: Rust's iterators are mutable (progress is tracked in &mut self); OCaml's Seq.t is immutable and persistent.
  • Eagerness: Both are lazy by default; Rust materializes with .collect(), OCaml with Seq.to_list or Seq.fold_left.
  • Adapter count: Rust's Iterator has 75+ adapters as defaults; OCaml's Seq has ~20 functions, with more in Base.Sequence.
  • OCaml Approach

    OCaml's Seq.t is the lazy sequence type: type 'a t = unit -> 'a node where node = Nil | Cons of 'a * 'a t. A Fibonacci sequence is let rec fib a b = fun () -> Seq.Cons (a, fib b (a+b)). The Seq module provides map, filter, take, fold_left adapters. Unlike Rust's iterator, OCaml sequences are persistent (can be consumed multiple times). The Iter module provides mutable imperative iterators closer to Rust's model.

    Full Source

    #![allow(clippy::all)]
    //! Iterator Trait Deep Dive
    //!
    //! Advanced iterator patterns, adapters, and custom iterators.
    
    /// Custom Fibonacci iterator.
    pub struct Fibonacci {
        curr: u64,
        next: u64,
    }
    
    impl Fibonacci {
        /// Creates a new Fibonacci iterator starting with 1, 1, 2, 3, ...
        pub fn new() -> Self {
            Fibonacci { curr: 0, next: 1 }
        }
    }
    
    impl Default for Fibonacci {
        fn default() -> Self {
            Self::new()
        }
    }
    
    impl Iterator for Fibonacci {
        type Item = u64;
    
        fn next(&mut self) -> Option<Self::Item> {
            let new_next = self.curr + self.next;
            self.curr = self.next;
            self.next = new_next;
            Some(self.curr)
        }
    }
    
    /// Custom iterator that counts down from n to 1.
    pub struct Countdown {
        current: u32,
    }
    
    impl Countdown {
        pub fn new(start: u32) -> Self {
            Countdown { current: start }
        }
    }
    
    impl Iterator for Countdown {
        type Item = u32;
    
        fn next(&mut self) -> Option<Self::Item> {
            if self.current > 0 {
                let value = self.current;
                self.current -= 1;
                Some(value)
            } else {
                None
            }
        }
    
        // Provide size_hint for efficiency
        fn size_hint(&self) -> (usize, Option<usize>) {
            let remaining = self.current as usize;
            (remaining, Some(remaining))
        }
    }
    
    impl ExactSizeIterator for Countdown {}
    
    /// Approach 1: Using standard combinators for sum of squares of evens.
    pub fn sum_of_squares_of_evens(n: i32) -> i32 {
        (1..=n).filter(|x| x % 2 == 0).map(|x| x * x).sum()
    }
    
    /// Approach 2: Using fold for more control.
    pub fn sum_of_squares_of_evens_fold(n: i32) -> i32 {
        (1..=n).fold(0, |acc, x| if x % 2 == 0 { acc + x * x } else { acc })
    }
    
    /// Demonstrates flat_map for Cartesian product.
    pub fn pairs_where_x_less_than_y(n: i32) -> Vec<(i32, i32)> {
        (1..=n)
            .flat_map(|x| (1..=n).map(move |y| (x, y)))
            .filter(|(x, y)| x < y)
            .collect()
    }
    
    /// Demonstrates scan for running totals.
    pub fn running_sum(values: &[i32]) -> Vec<i32> {
        values
            .iter()
            .scan(0, |state, &x| {
                *state += x;
                Some(*state)
            })
            .collect()
    }
    
    /// Demonstrates take_while.
    pub fn take_while_positive(values: &[i32]) -> Vec<i32> {
        values.iter().take_while(|&&x| x > 0).copied().collect()
    }
    
    /// Demonstrates skip_while.
    pub fn skip_while_small(values: &[i32], threshold: i32) -> Vec<i32> {
        values
            .iter()
            .skip_while(|&&x| x < threshold)
            .copied()
            .collect()
    }
    
    /// Demonstrates chain for concatenation.
    pub fn chain_ranges(
        a: std::ops::RangeInclusive<i32>,
        b: std::ops::RangeInclusive<i32>,
    ) -> Vec<i32> {
        a.chain(b).collect()
    }
    
    /// Demonstrates partition.
    pub fn partition_even_odd(n: i32) -> (Vec<i32>, Vec<i32>) {
        (1..=n).partition(|x| x % 2 == 0)
    }
    
    /// Demonstrates zip.
    pub fn zip_with_index<T: Clone>(items: &[T]) -> Vec<(usize, T)> {
        (0..).zip(items.iter().cloned()).collect()
    }
    
    /// Demonstrates unzip.
    pub fn unzip_pairs<A: Clone, B: Clone>(pairs: Vec<(A, B)>) -> (Vec<A>, Vec<B>) {
        pairs.into_iter().unzip()
    }
    
    /// Demonstrates peekable for lookahead.
    pub fn group_consecutive(values: &[i32]) -> Vec<Vec<i32>> {
        let mut result = Vec::new();
        let mut iter = values.iter().peekable();
    
        while let Some(&first) = iter.next() {
            let mut group = vec![first];
            while iter.peek() == Some(&&first) {
                iter.next();
                group.push(first);
            }
            result.push(group);
        }
    
        result
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_fibonacci() {
            let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
            assert_eq!(fibs, vec![1, 1, 2, 3, 5, 8, 13, 21, 34, 55]);
        }
    
        #[test]
        fn test_countdown() {
            let count: Vec<u32> = Countdown::new(5).collect();
            assert_eq!(count, vec![5, 4, 3, 2, 1]);
        }
    
        #[test]
        fn test_countdown_exact_size() {
            let cd = Countdown::new(10);
            assert_eq!(cd.len(), 10);
        }
    
        #[test]
        fn test_sum_of_squares_of_evens() {
            // 2² + 4² + 6² + 8² + 10² = 4 + 16 + 36 + 64 + 100 = 220
            assert_eq!(sum_of_squares_of_evens(10), 220);
            assert_eq!(sum_of_squares_of_evens_fold(10), 220);
        }
    
        #[test]
        fn test_pairs_where_x_less_than_y() {
            let pairs = pairs_where_x_less_than_y(3);
            assert_eq!(pairs, vec![(1, 2), (1, 3), (2, 3)]);
        }
    
        #[test]
        fn test_running_sum() {
            assert_eq!(running_sum(&[1, 2, 3, 4]), vec![1, 3, 6, 10]);
        }
    
        #[test]
        fn test_take_while_positive() {
            assert_eq!(take_while_positive(&[1, 2, 3, -1, 4, 5]), vec![1, 2, 3]);
        }
    
        #[test]
        fn test_skip_while_small() {
            assert_eq!(skip_while_small(&[1, 2, 5, 3, 6], 4), vec![5, 3, 6]);
        }
    
        #[test]
        fn test_chain_ranges() {
            assert_eq!(chain_ranges(1..=3, 7..=9), vec![1, 2, 3, 7, 8, 9]);
        }
    
        #[test]
        fn test_partition_even_odd() {
            let (evens, odds) = partition_even_odd(6);
            assert_eq!(evens, vec![2, 4, 6]);
            assert_eq!(odds, vec![1, 3, 5]);
        }
    
        #[test]
        fn test_zip_with_index() {
            let items = vec!["a", "b", "c"];
            let indexed = zip_with_index(&items);
            assert_eq!(indexed, vec![(0, "a"), (1, "b"), (2, "c")]);
        }
    
        #[test]
        fn test_unzip_pairs() {
            let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
            let (letters, numbers) = unzip_pairs(pairs);
            assert_eq!(letters, vec!["a", "b", "c"]);
            assert_eq!(numbers, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_group_consecutive() {
            let groups = group_consecutive(&[1, 1, 2, 3, 3, 3, 1]);
            assert_eq!(groups, vec![vec![1, 1], vec![2], vec![3, 3, 3], vec![1]]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_fibonacci() {
            let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
            assert_eq!(fibs, vec![1, 1, 2, 3, 5, 8, 13, 21, 34, 55]);
        }
    
        #[test]
        fn test_countdown() {
            let count: Vec<u32> = Countdown::new(5).collect();
            assert_eq!(count, vec![5, 4, 3, 2, 1]);
        }
    
        #[test]
        fn test_countdown_exact_size() {
            let cd = Countdown::new(10);
            assert_eq!(cd.len(), 10);
        }
    
        #[test]
        fn test_sum_of_squares_of_evens() {
            // 2² + 4² + 6² + 8² + 10² = 4 + 16 + 36 + 64 + 100 = 220
            assert_eq!(sum_of_squares_of_evens(10), 220);
            assert_eq!(sum_of_squares_of_evens_fold(10), 220);
        }
    
        #[test]
        fn test_pairs_where_x_less_than_y() {
            let pairs = pairs_where_x_less_than_y(3);
            assert_eq!(pairs, vec![(1, 2), (1, 3), (2, 3)]);
        }
    
        #[test]
        fn test_running_sum() {
            assert_eq!(running_sum(&[1, 2, 3, 4]), vec![1, 3, 6, 10]);
        }
    
        #[test]
        fn test_take_while_positive() {
            assert_eq!(take_while_positive(&[1, 2, 3, -1, 4, 5]), vec![1, 2, 3]);
        }
    
        #[test]
        fn test_skip_while_small() {
            assert_eq!(skip_while_small(&[1, 2, 5, 3, 6], 4), vec![5, 3, 6]);
        }
    
        #[test]
        fn test_chain_ranges() {
            assert_eq!(chain_ranges(1..=3, 7..=9), vec![1, 2, 3, 7, 8, 9]);
        }
    
        #[test]
        fn test_partition_even_odd() {
            let (evens, odds) = partition_even_odd(6);
            assert_eq!(evens, vec![2, 4, 6]);
            assert_eq!(odds, vec![1, 3, 5]);
        }
    
        #[test]
        fn test_zip_with_index() {
            let items = vec!["a", "b", "c"];
            let indexed = zip_with_index(&items);
            assert_eq!(indexed, vec![(0, "a"), (1, "b"), (2, "c")]);
        }
    
        #[test]
        fn test_unzip_pairs() {
            let pairs = vec![("a", 1), ("b", 2), ("c", 3)];
            let (letters, numbers) = unzip_pairs(pairs);
            assert_eq!(letters, vec!["a", "b", "c"]);
            assert_eq!(numbers, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_group_consecutive() {
            let groups = group_consecutive(&[1, 1, 2, 3, 3, 3, 1]);
            assert_eq!(groups, vec![vec![1, 1], vec![2], vec![3, 3, 3], vec![1]]);
        }
    }

    Deep Comparison

    OCaml vs Rust: Iterator Trait Deep Dive

    Side-by-Side Code

    OCaml — Seq module

    let range a b = Seq.init (b - a) (fun i -> a + i)
    
    let sum_of_squares_of_evens =
      range 1 11
      |> Seq.filter (fun x -> x mod 2 = 0)
      |> Seq.map (fun x -> x * x)
      |> Seq.fold_left (+) 0
    
    (* flat_map *)
    let pairs =
      range 1 4
      |> Seq.flat_map (fun x -> Seq.map (fun y -> (x, y)) (range 1 4))
      |> Seq.filter (fun (x, y) -> x < y)
      |> List.of_seq
    
    (* zip *)
    let names = List.to_seq ["Alice"; "Bob"; "Carol"]
    let scores = List.to_seq [95; 87; 91]
    let combined = Seq.zip names scores
    

    Rust — Iterator trait

    // Pipeline
    let sum: i32 = (1..=10)
        .filter(|x| x % 2 == 0)
        .map(|x| x * x)
        .sum();
    
    // flat_map
    let pairs: Vec<(i32, i32)> = (1..=3)
        .flat_map(|x| (1..=3).map(move |y| (x, y)))
        .filter(|(x, y)| x < y)
        .collect();
    
    // zip
    let names = ["Alice", "Bob", "Carol"];
    let scores = [95, 87, 91];
    let combined: Vec<_> = names.iter().zip(scores.iter()).collect();
    

    Comparison Table

    AdapterOCaml (Seq)Rust (Iterator)
    TransformSeq.map f.map(f)
    FilterSeq.filter p.filter(p)
    ReduceSeq.fold_left f init.fold(init, f)
    FlattenSeq.flat_map f.flat_map(f)
    Take first nSeq.take n.take(n)
    ZipSeq.zip a ba.zip(b)
    CollectList.of_seq.collect()
    SumSeq.fold_left (+) 0.sum()
    FindSeq.find p.find(p)

    Custom Iterators

    OCaml — Using Seq

    let rec fibonacci a b =
      fun () -> Seq.Cons (a, fibonacci b (a + b))
    
    let fibs = fibonacci 1 1
    let first_10 = fibs |> Seq.take 10 |> List.of_seq
    

    Rust — Implementing Iterator

    struct Fibonacci { curr: u64, next: u64 }
    
    impl Iterator for Fibonacci {
        type Item = u64;
        fn next(&mut self) -> Option<Self::Item> {
            let new_next = self.curr + self.next;
            self.curr = self.next;
            self.next = new_next;
            Some(self.curr)
        }
    }
    
    let fibs: Vec<u64> = Fibonacci { curr: 0, next: 1 }.take(10).collect();
    

    Laziness

    Both OCaml's Seq and Rust's Iterator are lazy — no work is done until consumed:

    // Nothing happens yet
    let iter = (1..1_000_000).filter(|x| x % 2 == 0).map(|x| x * x);
    
    // Now it runs, but only processes first 5
    let result: Vec<_> = iter.take(5).collect();
    

    OCaml's Seq is similarly lazy:

    let iter = range 1 1000000
      |> Seq.filter (fun x -> x mod 2 = 0)
      |> Seq.map (fun x -> x * x)
    
    let result = iter |> Seq.take 5 |> List.of_seq
    

    Stateful Iteration

    Rust — scan

    // Running sum
    let running: Vec<i32> = (1..=4)
        .scan(0, |state, x| { *state += x; Some(*state) })
        .collect();
    // [1, 3, 6, 10]
    

    OCaml — scan equivalent

    let scan f init seq =
      let state = ref init in
      Seq.map (fun x -> state := f !state x; !state) seq
    
    let running = range 1 5 |> scan (+) 0 |> List.of_seq
    

    5 Takeaways

  • Both are lazy by default.
  • No computation until .collect() / List.of_seq.

  • **Rust's move keyword captures variables in closures.**
  • flat_map(|x| (1..=n).map(move |y| (x, y)))move gives the inner closure ownership.

  • Custom iterators are straightforward in Rust.
  • Implement Iterator with type Item and fn next().

  • Rust has more built-in adapters.
  • scan, peekable, enumerate, partition, unzip — all in std.

  • Type inference works across iterator chains.
  • Rust knows the types through the entire pipeline.

    Exercises

  • Primes iterator: Implement struct Primes { current: u64 } implementing Iterator<Item = u64> using trial division. The iterator yields successive prime numbers. Write a test collecting the first 10 primes.
  • Zip with index (enumerate): Without using .enumerate(), implement a Numbered<I: Iterator> wrapper that implements Iterator<Item = (usize, I::Item)>. Verify it matches .enumerate() output exactly.
  • Infinite spiral: Implement a Spiral iterator that yields (i32, i32) coordinates in a clockwise spiral from (0,0): (0,0), (1,0), (1,-1), (0,-1), (-1,-1), (-1,0), (-1,1), (0,1), (1,1), (2,1), ... Use it with .take(25) to verify the spiral pattern.
  • Open Source Repos