ExamplesBy LevelBy TopicLearning Paths
087 Intermediate

087 — Iterator Adapters

Functional Programming

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

  • • Model an iterator adapter as a struct holding an inner iterator and a function
  • • Use FnMut for adapter closures that may need mutable state (e.g. counting)
  • • Implement next for adapters by delegating to the inner iterator
  • • Create an extension trait to add adapter methods to all iterators
  • • Understand why adapter structs are generic over I: Iterator and F: FnMut
  • • Map Rust's adapter structs to OCaml's higher-order Seq-based functions
  • Code 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

    AspectRustOCaml
    Adapter typeGeneric struct MyMap<I, F>Higher-order function 'a Seq.t -> 'a Seq.t
    Closure mutabilityFnMut allowedMutable via ref if needed
    Extensiontrait MyIterExt|> pipe operator
    Filter inner loopwhile letTail-recursive my_filter p rest ()
    ComposabilityFluent method chain|> composition
    Laziness mechanismPull-based nextThunk 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]);
        }
    }
    ✓ Tests Rust test suite
    #[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 Seq
  • • Can build custom: let my_map f s () = match s () with ...
  • Rust Approach

  • • Built-in: .map(), .filter(), .take(), .skip()
  • • Custom: struct wrapping I: Iterator + impl Iterator
  • Comparison Table

    AdapterOCamlRust
    MapSeq.map f s.map(f)
    FilterSeq.filter p s.filter(p)
    Takemanual.take(n)
    Skipmanual.skip(n)
    CustomClosure-basedStruct wrapping iterator

    Exercises

  • Implement a MyZip<A: Iterator, B: Iterator> adapter that yields pairs (A::Item, B::Item) and stops when either iterator is exhausted.
  • Write a MyFlatMap<I, F, J> adapter where F: FnMut(I::Item) -> J and J: Iterator.
  • Add a my_enumerate method to MyIterExt that wraps items as (usize, Item).
  • Implement MyChain<A: Iterator<Item=T>, B: Iterator<Item=T>> that yields all of A then all of B.
  • In OCaml, write a 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.
  • Open Source Repos