Skip to Content
Course content

138: Practice Exercise: Building a Custom Iterator Adapter

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, we've spent a lot of time using the built-in iterator adapters like map, filter, and take. They're incredibly powerful, but eventually, you'll run into a situation where the standard library doesn't have exactly what you need. When that happens, you don't just write a for loop and dump everything into a Vec—you build your own adapter. This keeps your code lazy and composable.

I want to build a EveryNth adapter. The idea is simple: it wraps another iterator and only yields every Nth element. If N is 3, we get the 1st, 4th, 7th elements, and so on.

Wrapping the underlying iterator

To make this an "adapter," our struct needs to own (or hold a reference to) another iterator. Since we want this to work with any kind of iterator—be it a Range, a VecIter, or something else—we have to use generics. I'll define a struct that holds the iterator and a counter for the skip interval.

struct EveryNth<I> {
    iter: I,
    n: usize,
}

impl<I: Iterator> EveryNth<I> {
    fn new(iter: I, n: usize) -> Self {
        Self { iter, n }
    }
}

Note that I'm constraining I to Iterator only when implementing the logic. It's a habit of mine to keep the struct definition as loose as possible and put the bounds on the impl blocks.

Implementing the Iterator trait

Now for the heavy lifting. We need to implement Iterator for EveryNth. The next method is where the magic happens. My initial thought is to just loop n times and return the last element we found.

impl<I: Iterator> Iterator for EveryNth<I> {
    type Item = I::Item;

    fn next(&mut self) -> Option<Self::Item> {
        for _ in 0..self.n {
            return self.iter.next();
        }
        None
    }
}

Wait, I broke it

If I ran this, I'd realize immediately that it's completely wrong. I just put a return inside the first iteration of the loop. It's not skipping anything; it's just acting like a proxy for the original iterator. Even if I removed the return and tried to store the value, I'd run into a problem: what happens if the underlying iterator ends halfway through the skip loop?

Let's fix the logic. We need to consume n - 1 elements and then return the nth one. If we hit None at any point during the skip, the whole thing is over.

impl<I: Iterator> Iterator for EveryNth<I> {
    type Item = I::Item;

    fn next(&mut self) -> Option<Self::Item> {
        // We want to skip n-1 elements
        for _ in 0..self.n - 1 {
            if self.iter.next().is_none() {
                return None;
            }
        }
        // The nth element is the one we actually yield
        self.iter.next()
    }
}

That's much better. I'm using is_none() to bail out early if the source iterator runs dry before we reach the element we actually care about.

Making it feel like a native Rust adapter

Right now, using this is a bit clunky: EveryNth::new(vec.into_iter(), 3). In Rust, we usually provide a trait to make these adapters callable directly on any iterator. I'll create a custom trait and implement it for every Iterator.

trait EveryNthExt: Iterator {
    fn every_nth(self, n: usize) -> EveryNth<Self> {
        EveryNth::new(self, n)
    }
}

impl<I: Iterator> EveryNthExt for I {}

Now I can chain it just like map or filter. I'll test it out with a simple range:

fn main() {
    let result: Vec<_> = (1..10).every_nth(3).collect();
    println!<{:?}>; // Output: [1, 4, 7]
}

I'm pretty happy with this. It's lazy, it doesn't allocate any extra memory regardless of how large the input sequence is, and it fits perfectly into the iterator pipeline.




📋 Practical Task

Exercise: Building an Interleave Iterator Adapter

Now it's your turn. You're going to build an Interleave adapter. This adapter should take two different iterators of the same item type and yield elements from them alternately (one from the first, one from the second, and so on).

Requirements:

  • Create a struct Interleave<I1, I2> that wraps two iterators.
  • Implement the Iterator trait for Interleave.
  • The next method should alternate between the two iterators.
  • If one iterator is exhausted before the other, the adapter should continue yielding elements from the remaining iterator until both are empty.
  • Implement a trait InterleaveExt so you can call .interleave(other_iter) on any iterator.
Testing your implementation:

Your code should produce the following result:

let a = vec![1, 3, 5];
let b = vec![2, 4, 6, 8, 10];
let result: Vec<_> = a.into_iter().interleave(b.into_iter()).collect();
assert_eq!(result, vec![1, 2, 3, 4, 5, 6, 8, 10]);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.