087 — Iterator Adapters
Tutorial
The Problem
Implement custom iterator adapters — MyMap, MyFilter, and MyTake — by wrapping an inner iterator and implementing next. Bundle them into an extension trait MyIterExt for fluent chaining. Compare with OCaml's Seq-based my_map, my_filter, and my_take functions.
🎯 Learning Outcomes
FnMut for adapter closures that may need mutable state (e.g. counting)next for adapters by delegating to the inner iteratorI: Iterator and F: FnMutSeq-based functionsCode Example
#![allow(clippy::all)]
// 087: Iterator Adapters — build custom adapters
// Approach 1: Custom Map adapter
struct MyMap<I, F> {
iter: I,
f: F,
}
impl<I, F, B> Iterator for MyMap<I, F>
where
I: Iterator,
F: FnMut(I::Item) -> B,
{
type Item = B;
fn next(&mut self) -> Option<B> {
self.iter.next().map(&mut self.f)
}
}
// Approach 2: Custom Filter adapter
struct MyFilter<I, P> {
iter: I,
predicate: P,
}
impl<I, P> Iterator for MyFilter<I, P>
where
I: Iterator,
P: FnMut(&I::Item) -> bool,
{
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
while let Some(item) = self.iter.next() {
if (self.predicate)(&item) {
return Some(item);
}
}
None
}
}
// Approach 3: Custom Take adapter
struct MyTake<I> {
iter: I,
remaining: usize,
}
impl<I: Iterator> Iterator for MyTake<I> {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
if self.remaining == 0 {
None
} else {
self.remaining -= 1;
self.iter.next()
}
}
}
// Extension trait for fluent API
trait MyIterExt: Iterator + Sized {
fn my_map<F, B>(self, f: F) -> MyMap<Self, F>
where
F: FnMut(Self::Item) -> B,
{
MyMap { iter: self, f }
}
fn my_filter<P>(self, predicate: P) -> MyFilter<Self, P>
where
P: FnMut(&Self::Item) -> bool,
{
MyFilter {
iter: self,
predicate,
}
}
fn my_take(self, n: usize) -> MyTake<Self> {
MyTake {
iter: self,
remaining: n,
}
}
}
impl<I: Iterator> MyIterExt for I {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_my_map() {
let v: Vec<i32> = (0..5).my_map(|x| x * 2).collect();
assert_eq!(v, vec![0, 2, 4, 6, 8]);
}
#[test]
fn test_my_filter() {
let v: Vec<i32> = (0..5).my_filter(|x| x > &2).collect();
assert_eq!(v, vec![3, 4]);
}
#[test]
fn test_my_take() {
let v: Vec<i32> = (0..100).my_take(3).collect();
assert_eq!(v, vec![0, 1, 2]);
}
#[test]
fn test_compose() {
let v: Vec<i32> = (0..)
.my_filter(|x| x % 2 == 0)
.my_map(|x| x * x)
.my_take(5)
.collect();
assert_eq!(v, vec![0, 4, 16, 36, 64]);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Adapter type | Generic struct MyMap<I, F> | Higher-order function 'a Seq.t -> 'a Seq.t |
| Closure mutability | FnMut allowed | Mutable via ref if needed |
| Extension | trait MyIterExt | |> pipe operator |
| Filter inner loop | while let | Tail-recursive my_filter p rest () |
| Composability | Fluent method chain | |> composition |
| Laziness mechanism | Pull-based next | Thunk evaluation |
Building custom adapters from scratch reveals the mechanics behind std::iter. In production, use the standard adapters — they are optimised and tested. But understanding the internals is essential for designing custom data sources and specialised iteration patterns.
OCaml Approach
OCaml's Seq adapters are plain functions: my_map f s returns a thunk fun () -> match s () with Seq.Nil -> … | Seq.Cons(x, rest) -> Seq.Cons(f x, my_map f rest). my_filter recurses to skip non-matching elements. my_take decrements a counter. There is no extension trait; composition is achieved with the |> pipe operator. Both approaches achieve the same lazy pipeline; Rust's adapter structs make the type of each step explicit.
Full Source
#![allow(clippy::all)]
// 087: Iterator Adapters — build custom adapters
// Approach 1: Custom Map adapter
struct MyMap<I, F> {
iter: I,
f: F,
}
impl<I, F, B> Iterator for MyMap<I, F>
where
I: Iterator,
F: FnMut(I::Item) -> B,
{
type Item = B;
fn next(&mut self) -> Option<B> {
self.iter.next().map(&mut self.f)
}
}
// Approach 2: Custom Filter adapter
struct MyFilter<I, P> {
iter: I,
predicate: P,
}
impl<I, P> Iterator for MyFilter<I, P>
where
I: Iterator,
P: FnMut(&I::Item) -> bool,
{
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
while let Some(item) = self.iter.next() {
if (self.predicate)(&item) {
return Some(item);
}
}
None
}
}
// Approach 3: Custom Take adapter
struct MyTake<I> {
iter: I,
remaining: usize,
}
impl<I: Iterator> Iterator for MyTake<I> {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
if self.remaining == 0 {
None
} else {
self.remaining -= 1;
self.iter.next()
}
}
}
// Extension trait for fluent API
trait MyIterExt: Iterator + Sized {
fn my_map<F, B>(self, f: F) -> MyMap<Self, F>
where
F: FnMut(Self::Item) -> B,
{
MyMap { iter: self, f }
}
fn my_filter<P>(self, predicate: P) -> MyFilter<Self, P>
where
P: FnMut(&Self::Item) -> bool,
{
MyFilter {
iter: self,
predicate,
}
}
fn my_take(self, n: usize) -> MyTake<Self> {
MyTake {
iter: self,
remaining: n,
}
}
}
impl<I: Iterator> MyIterExt for I {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_my_map() {
let v: Vec<i32> = (0..5).my_map(|x| x * 2).collect();
assert_eq!(v, vec![0, 2, 4, 6, 8]);
}
#[test]
fn test_my_filter() {
let v: Vec<i32> = (0..5).my_filter(|x| x > &2).collect();
assert_eq!(v, vec![3, 4]);
}
#[test]
fn test_my_take() {
let v: Vec<i32> = (0..100).my_take(3).collect();
assert_eq!(v, vec![0, 1, 2]);
}
#[test]
fn test_compose() {
let v: Vec<i32> = (0..)
.my_filter(|x| x % 2 == 0)
.my_map(|x| x * x)
.my_take(5)
.collect();
assert_eq!(v, vec![0, 4, 16, 36, 64]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_my_map() {
let v: Vec<i32> = (0..5).my_map(|x| x * 2).collect();
assert_eq!(v, vec![0, 2, 4, 6, 8]);
}
#[test]
fn test_my_filter() {
let v: Vec<i32> = (0..5).my_filter(|x| x > &2).collect();
assert_eq!(v, vec![3, 4]);
}
#[test]
fn test_my_take() {
let v: Vec<i32> = (0..100).my_take(3).collect();
assert_eq!(v, vec![0, 1, 2]);
}
#[test]
fn test_compose() {
let v: Vec<i32> = (0..)
.my_filter(|x| x % 2 == 0)
.my_map(|x| x * x)
.my_take(5)
.collect();
assert_eq!(v, vec![0, 4, 16, 36, 64]);
}
}
Deep Comparison
Core Insight
An adapter takes an iterator and returns a new iterator that transforms each element. They compose lazily — no intermediate collections.
OCaml Approach
Seq.map, Seq.filter — return new Seqlet my_map f s () = match s () with ...Rust Approach
.map(), .filter(), .take(), .skip()I: Iterator + impl IteratorComparison Table
| Adapter | OCaml | Rust |
|---|---|---|
| Map | Seq.map f s | .map(f) |
| Filter | Seq.filter p s | .filter(p) |
| Take | manual | .take(n) |
| Skip | manual | .skip(n) |
| Custom | Closure-based | Struct wrapping iterator |
Exercises
MyZip<A: Iterator, B: Iterator> adapter that yields pairs (A::Item, B::Item) and stops when either iterator is exhausted.MyFlatMap<I, F, J> adapter where F: FnMut(I::Item) -> J and J: Iterator.my_enumerate method to MyIterExt that wraps items as (usize, Item).MyChain<A: Iterator<Item=T>, B: Iterator<Item=T>> that yields all of A then all of B.seq_flat_map : ('a -> 'b Seq.t) -> 'a Seq.t -> 'b Seq.t and test it on a sequence of string words split into characters.