405: Iterator Trait Deep Dive
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
fn next unlocks the entire iterator adapter librarysize_hint enables efficient pre-allocation in .collect()for or a terminal operationstd 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
next; OCaml's Seq.t is a recursive type that requires understanding the node type structure.&mut self); OCaml's Seq.t is immutable and persistent..collect(), OCaml with Seq.to_list or Seq.fold_left.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]]);
}
}#[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
| Adapter | OCaml (Seq) | Rust (Iterator) |
|---|---|---|
| Transform | Seq.map f | .map(f) |
| Filter | Seq.filter p | .filter(p) |
| Reduce | Seq.fold_left f init | .fold(init, f) |
| Flatten | Seq.flat_map f | .flat_map(f) |
| Take first n | Seq.take n | .take(n) |
| Zip | Seq.zip a b | a.zip(b) |
| Collect | List.of_seq | .collect() |
| Sum | Seq.fold_left (+) 0 | .sum() |
| Find | Seq.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
No computation until .collect() / List.of_seq.
move keyword captures variables in closures.** flat_map(|x| (1..=n).map(move |y| (x, y))) — move gives the inner closure ownership.
Implement Iterator with type Item and fn next().
scan, peekable, enumerate, partition, unzip — all in std.
Rust knows the types through the entire pipeline.
Exercises
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..enumerate(), implement a Numbered<I: Iterator> wrapper that implements Iterator<Item = (usize, I::Item)>. Verify it matches .enumerate() output exactly.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.