086 — Custom Iterator with State
Tutorial
The Problem
Implement three stateful custom iterators: a Counter that steps by a given value indefinitely, a Fib Fibonacci iterator, and a Collatz iterator that terminates at 1. Demonstrate that implementing next unlocks the full iterator adapter chain. Compare with OCaml's closure-based counters and Seq lazy sequences.
🎯 Learning Outcomes
Some(val) forever from an infinite iterator; use .take(n) to bound itdone_ flag and returning Nonemutable ref closure and Seq thunk.zip, .map, .sumCode Example
#![allow(clippy::all)]
// 086: Custom Iterator with State
// Approach 1: Counter iterator
struct Counter {
current: i32,
step: i32,
}
impl Counter {
fn new(start: i32, step: i32) -> Self {
Counter {
current: start,
step,
}
}
}
impl Iterator for Counter {
type Item = i32;
fn next(&mut self) -> Option<i32> {
self.current += self.step;
Some(self.current) // infinite
}
}
// Approach 2: Fibonacci iterator
struct Fib {
a: u64,
b: u64,
}
impl Fib {
fn new() -> Self {
Fib { a: 0, b: 1 }
}
}
impl Iterator for Fib {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let val = self.a;
let next = self.a + self.b;
self.a = self.b;
self.b = next;
Some(val)
}
}
// Approach 3: Collatz sequence (finite)
struct Collatz {
n: u64,
done_: bool,
}
impl Collatz {
fn new(start: u64) -> Self {
Collatz {
n: start,
done_: false,
}
}
}
impl Iterator for Collatz {
type Item = u64;
fn next(&mut self) -> Option<u64> {
if self.done_ {
return None;
}
let val = self.n;
if self.n == 1 {
self.done_ = true;
} else if self.n.is_multiple_of(2) {
self.n /= 2;
} else {
self.n = 3 * self.n + 1;
}
Some(val)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter() {
let v: Vec<i32> = Counter::new(0, 2).take(3).collect();
assert_eq!(v, vec![2, 4, 6]);
}
#[test]
fn test_counter_negative() {
let v: Vec<i32> = Counter::new(10, -3).take(4).collect();
assert_eq!(v, vec![7, 4, 1, -2]);
}
#[test]
fn test_fibonacci() {
let fibs: Vec<u64> = Fib::new().take(8).collect();
assert_eq!(fibs, vec![0, 1, 1, 2, 3, 5, 8, 13]);
}
#[test]
fn test_collatz() {
let v: Vec<u64> = Collatz::new(6).collect();
assert_eq!(v, vec![6, 3, 10, 5, 16, 8, 4, 2, 1]);
}
#[test]
fn test_collatz_one() {
let v: Vec<u64> = Collatz::new(1).collect();
assert_eq!(v, vec![1]);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| State storage | Struct fields | Mutable ref or closure capture |
| Infinite | Return Some(…) always | Seq.Cons(…, thunk) always |
| Finite | Return None at end | Seq.Nil at end |
| Lazy | Pull-based (call next) | Thunk-based (call seq ()) |
| Adapter chain | Free from Iterator trait | Manual seq_map/seq_filter |
| Termination signal | done_ flag or pattern on value | Seq.Nil |
The Collatz iterator demonstrates a common pattern: an iterator that terminates when some condition on the internal state is met. This is cleaner than computing the full sequence upfront and is memory-efficient for long sequences.
OCaml Approach
OCaml uses mutable ref cells for stateful counters: let n = ref (start - step) in fun () -> n := !n + step; Some !n. Fibonacci uses the Seq module: let rec aux a b () = Seq.Cons(a, aux b (a+b)). The take_seq helper materialises the first n elements. Collatz is also expressed as a Seq with termination when n = 1. OCaml's approach requires explicit Seq.Cons/Seq.Nil construction while Rust's Option<T> return is simpler.
Full Source
#![allow(clippy::all)]
// 086: Custom Iterator with State
// Approach 1: Counter iterator
struct Counter {
current: i32,
step: i32,
}
impl Counter {
fn new(start: i32, step: i32) -> Self {
Counter {
current: start,
step,
}
}
}
impl Iterator for Counter {
type Item = i32;
fn next(&mut self) -> Option<i32> {
self.current += self.step;
Some(self.current) // infinite
}
}
// Approach 2: Fibonacci iterator
struct Fib {
a: u64,
b: u64,
}
impl Fib {
fn new() -> Self {
Fib { a: 0, b: 1 }
}
}
impl Iterator for Fib {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let val = self.a;
let next = self.a + self.b;
self.a = self.b;
self.b = next;
Some(val)
}
}
// Approach 3: Collatz sequence (finite)
struct Collatz {
n: u64,
done_: bool,
}
impl Collatz {
fn new(start: u64) -> Self {
Collatz {
n: start,
done_: false,
}
}
}
impl Iterator for Collatz {
type Item = u64;
fn next(&mut self) -> Option<u64> {
if self.done_ {
return None;
}
let val = self.n;
if self.n == 1 {
self.done_ = true;
} else if self.n.is_multiple_of(2) {
self.n /= 2;
} else {
self.n = 3 * self.n + 1;
}
Some(val)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter() {
let v: Vec<i32> = Counter::new(0, 2).take(3).collect();
assert_eq!(v, vec![2, 4, 6]);
}
#[test]
fn test_counter_negative() {
let v: Vec<i32> = Counter::new(10, -3).take(4).collect();
assert_eq!(v, vec![7, 4, 1, -2]);
}
#[test]
fn test_fibonacci() {
let fibs: Vec<u64> = Fib::new().take(8).collect();
assert_eq!(fibs, vec![0, 1, 1, 2, 3, 5, 8, 13]);
}
#[test]
fn test_collatz() {
let v: Vec<u64> = Collatz::new(6).collect();
assert_eq!(v, vec![6, 3, 10, 5, 16, 8, 4, 2, 1]);
}
#[test]
fn test_collatz_one() {
let v: Vec<u64> = Collatz::new(1).collect();
assert_eq!(v, vec![1]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter() {
let v: Vec<i32> = Counter::new(0, 2).take(3).collect();
assert_eq!(v, vec![2, 4, 6]);
}
#[test]
fn test_counter_negative() {
let v: Vec<i32> = Counter::new(10, -3).take(4).collect();
assert_eq!(v, vec![7, 4, 1, -2]);
}
#[test]
fn test_fibonacci() {
let fibs: Vec<u64> = Fib::new().take(8).collect();
assert_eq!(fibs, vec![0, 1, 1, 2, 3, 5, 8, 13]);
}
#[test]
fn test_collatz() {
let v: Vec<u64> = Collatz::new(6).collect();
assert_eq!(v, vec![6, 3, 10, 5, 16, 8, 4, 2, 1]);
}
#[test]
fn test_collatz_one() {
let v: Vec<u64> = Collatz::new(1).collect();
assert_eq!(v, vec![1]);
}
}
Deep Comparison
Core Insight
Custom iterators encapsulate state. Each call to next() advances the internal state machine. This is more explicit than OCaml's closure-based sequences.
OCaml Approach
let r = ref 0 in fun () -> incr r; !rSeq thunksRust Approach
impl Iterator with next(&mut self)Comparison Table
| Feature | OCaml | Rust |
|---|---|---|
| State | Closure ref / pure threading | Struct fields |
| Mutation | ref / incr | &mut self |
| Infinite | Lazy Seq | Iterator (never returns None) |
Exercises
Prime iterator that produces prime numbers infinitely using a sieve stored in a HashSet.DoubleEndedIterator for Counter (since it has a known step, going backwards is well-defined).Zip3<A, B, C> iterator that zips three iterators into triples.RunLength<I: Iterator> adapter that wraps any iterator and emits (count, value) pairs.Seq that streams primes without storing the full sieve.