285: Building Custom Iterator Adapters
Tutorial Video
Text description (accessibility)
This video demonstrates the "285: Building Custom Iterator Adapters" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The standard library's `map`, `filter`, and `zip` adapters cover many cases, but domain-specific transformations often need their own adapters — a rate limiter that throttles output, a deduplicator that removes consecutive duplicates, or a strider that yields every nth element. Key difference from OCaml: 1. **State storage**: Rust adapters store state in struct fields; OCaml adapters capture state in closure variables.
Tutorial
The Problem
The standard library's map, filter, and zip adapters cover many cases, but domain-specific transformations often need their own adapters — a rate limiter that throttles output, a deduplicator that removes consecutive duplicates, or a strider that yields every nth element. Building custom adapters in Rust follows the same pattern as the standard library: wrap an inner iterator in a struct and implement Iterator on it, making the adapter composable with the entire ecosystem.
🎯 Learning Outcomes
I: Iterator in a struct, implement Iterator on the wrapperEveryNth adapter that yields every nth element from any iterator<I: Iterator> to make adapters work with any iterator sourceCode Example
struct EveryNth<I> { inner: I, n: usize, count: usize }
impl<I: Iterator> Iterator for EveryNth<I> {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
loop {
let item = self.inner.next()?;
let emit = self.count % self.n == 0;
self.count += 1;
if emit { return Some(item); }
}
}
}Key Differences
Iterator trait and gain all standard adapters; OCaml functions on Seq.t gain all Seq module functions.Rayon parallel iterators if they also implement the right parallel traits.OCaml Approach
OCaml's Seq module allows custom adapters as functions returning new sequences. Since Seq.t is just unit -> node, a custom adapter is simply a function that wraps the original sequence:
let every_nth n seq =
let rec go i s () = match s () with
| Seq.Nil -> Seq.Nil
| Seq.Cons (x, rest) ->
if i mod n = 0 then Seq.Cons (x, go (i+1) rest)
else go (i+1) rest ()
in go 0 seq
Both approaches create composable, lazy transformations.
Full Source
#![allow(clippy::all)]
//! # Building Custom Iterator Adapters
//!
//! Custom adapters wrap an iterator in a struct and implement `Iterator` on it.
//! This is the same pattern used by `map`, `filter`, and `zip` in the standard library.
/// Yields every nth element starting from the first
pub struct EveryNth<I> {
inner: I,
n: usize,
count: usize,
}
impl<I: Iterator> EveryNth<I> {
pub fn new(inner: I, n: usize) -> Self {
assert!(n > 0, "n must be positive");
EveryNth { inner, n, count: 0 }
}
}
impl<I: Iterator> Iterator for EveryNth<I> {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
loop {
let item = self.inner.next()?;
let emit = self.count % self.n == 0;
self.count += 1;
if emit {
return Some(item);
}
}
}
}
/// Yields adjacent pairs (a, b) as a sliding window of 2
pub struct Pairs<I: Iterator> {
inner: I,
prev: Option<I::Item>,
}
impl<I: Iterator> Pairs<I>
where
I::Item: Clone,
{
pub fn new(mut inner: I) -> Self {
let prev = inner.next();
Pairs { inner, prev }
}
}
impl<I: Iterator> Iterator for Pairs<I>
where
I::Item: Clone,
{
type Item = (I::Item, I::Item);
fn next(&mut self) -> Option<Self::Item> {
let next = self.inner.next()?;
let prev = self.prev.replace(next.clone())?;
Some((prev, next))
}
}
/// Adapter that applies a function to each element, keeping only Some results
pub struct FilterMapWith<I, F> {
inner: I,
f: F,
}
impl<I, F, B> FilterMapWith<I, F>
where
I: Iterator,
F: FnMut(I::Item) -> Option<B>,
{
pub fn new(inner: I, f: F) -> Self {
FilterMapWith { inner, f }
}
}
impl<I, F, B> Iterator for FilterMapWith<I, F>
where
I: Iterator,
F: FnMut(I::Item) -> Option<B>,
{
type Item = B;
fn next(&mut self) -> Option<B> {
loop {
match (self.f)(self.inner.next()?) {
Some(b) => return Some(b),
None => continue,
}
}
}
}
/// Extension trait to add our adapters to all iterators
pub trait IteratorExt: Iterator + Sized {
fn every_nth(self, n: usize) -> EveryNth<Self> {
EveryNth::new(self, n)
}
fn pairs(self) -> Pairs<Self>
where
Self::Item: Clone,
{
Pairs::new(self)
}
fn filter_map_with<F, B>(self, f: F) -> FilterMapWith<Self, F>
where
F: FnMut(Self::Item) -> Option<B>,
{
FilterMapWith::new(self, f)
}
}
impl<I: Iterator> IteratorExt for I {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_every_nth_3() {
let result: Vec<i32> = (0..9).every_nth(3).collect();
assert_eq!(result, vec![0, 3, 6]);
}
#[test]
fn test_every_nth_2() {
let result: Vec<i32> = (0..10).every_nth(2).collect();
assert_eq!(result, vec![0, 2, 4, 6, 8]);
}
#[test]
fn test_pairs() {
let result: Vec<(i32, i32)> = [1i32, 2, 3, 4].iter().copied().pairs().collect();
assert_eq!(result, vec![(1, 2), (2, 3), (3, 4)]);
}
#[test]
fn test_pairs_single_element() {
let result: Vec<(i32, i32)> = [1i32].iter().copied().pairs().collect();
assert!(result.is_empty());
}
#[test]
fn test_every_nth_1_identity() {
let result: Vec<i32> = [1i32, 2, 3].iter().copied().every_nth(1).collect();
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_chain_adapters() {
let result: Vec<(i32, i32)> = (0i32..20).every_nth(2).pairs().collect();
assert_eq!(
result,
vec![
(0, 2),
(2, 4),
(4, 6),
(6, 8),
(8, 10),
(10, 12),
(12, 14),
(14, 16),
(16, 18)
]
);
}
#[test]
fn test_filter_map_with() {
let result: Vec<i32> = (0..10)
.filter_map_with(|x| if x % 2 == 0 { Some(x * 10) } else { None })
.collect();
assert_eq!(result, vec![0, 20, 40, 60, 80]);
}
#[test]
fn test_with_standard_adapters() {
let result: Vec<i32> = (0..20).filter(|x| x % 2 == 0).every_nth(2).collect();
assert_eq!(result, vec![0, 4, 8, 12, 16]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_every_nth_3() {
let result: Vec<i32> = (0..9).every_nth(3).collect();
assert_eq!(result, vec![0, 3, 6]);
}
#[test]
fn test_every_nth_2() {
let result: Vec<i32> = (0..10).every_nth(2).collect();
assert_eq!(result, vec![0, 2, 4, 6, 8]);
}
#[test]
fn test_pairs() {
let result: Vec<(i32, i32)> = [1i32, 2, 3, 4].iter().copied().pairs().collect();
assert_eq!(result, vec![(1, 2), (2, 3), (3, 4)]);
}
#[test]
fn test_pairs_single_element() {
let result: Vec<(i32, i32)> = [1i32].iter().copied().pairs().collect();
assert!(result.is_empty());
}
#[test]
fn test_every_nth_1_identity() {
let result: Vec<i32> = [1i32, 2, 3].iter().copied().every_nth(1).collect();
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_chain_adapters() {
let result: Vec<(i32, i32)> = (0i32..20).every_nth(2).pairs().collect();
assert_eq!(
result,
vec![
(0, 2),
(2, 4),
(4, 6),
(6, 8),
(8, 10),
(10, 12),
(12, 14),
(14, 16),
(16, 18)
]
);
}
#[test]
fn test_filter_map_with() {
let result: Vec<i32> = (0..10)
.filter_map_with(|x| if x % 2 == 0 { Some(x * 10) } else { None })
.collect();
assert_eq!(result, vec![0, 20, 40, 60, 80]);
}
#[test]
fn test_with_standard_adapters() {
let result: Vec<i32> = (0..20).filter(|x| x % 2 == 0).every_nth(2).collect();
assert_eq!(result, vec![0, 4, 8, 12, 16]);
}
}
Deep Comparison
OCaml vs Rust: Iterator Adapter Pattern
Pattern 1: Every Nth Element
OCaml
let every_nth n seq =
Seq.unfold (fun (i, rest) ->
let rec skip_to k s =
if k = 0 then
match Seq.uncons s with
| Some (v, rest') -> Some (v, (n-1, rest'))
| None -> None
else
match Seq.uncons s with
| Some (_, rest') -> skip_to (k-1) rest'
| None -> None
in
skip_to i rest
) (0, seq)
Rust
struct EveryNth<I> { inner: I, n: usize, count: usize }
impl<I: Iterator> Iterator for EveryNth<I> {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
loop {
let item = self.inner.next()?;
let emit = self.count % self.n == 0;
self.count += 1;
if emit { return Some(item); }
}
}
}
Pattern 2: Sliding Window Pairs
OCaml
let pairs seq =
Seq.unfold (fun s ->
match Seq.uncons s with
| None -> None
| Some (a, rest) ->
match Seq.uncons rest with
| None -> None
| Some (b, _) -> Some ((a, b), Seq.drop 1 s)
) seq
Rust
struct Pairs<I: Iterator> { inner: I, prev: Option<I::Item> }
impl<I: Iterator> Iterator for Pairs<I>
where I::Item: Clone
{
type Item = (I::Item, I::Item);
fn next(&mut self) -> Option<Self::Item> {
let next = self.inner.next()?;
let prev = self.prev.replace(next.clone())?;
Some((prev, next))
}
}
Pattern 3: Extension Trait
Rust
trait IteratorExt: Iterator + Sized {
fn every_nth(self, n: usize) -> EveryNth<Self> {
EveryNth::new(self, n)
}
fn pairs(self) -> Pairs<Self> where Self::Item: Clone {
Pairs::new(self)
}
}
impl<I: Iterator> IteratorExt for I {}
// Now usable on any iterator:
let result = (0..20).every_nth(3).pairs().collect();
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Adapter type | Function over Seq | Struct + impl Iterator |
| Extension method | Module or |> pipeline | Extension trait |
| Composability | Seq functions via |> | Method chaining |
| Type signature | 'a Seq.t -> 'b Seq.t | Adapter<I> where I: Iterator |
| Lazy evaluation | Seq is lazy | All iterators lazy |
Exercises
Deduplicate<I> adapter that yields consecutive elements only when they differ from the previous one (run-length deduplication).Buffered<I> adapter that collects elements into batches of size N and yields Vec<T> per batch.TimeoutIterator<I> adapter that stops yielding elements after a Duration has elapsed (simulated with an iteration count).