-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
138: Practice Exercise: Building a Custom Iterator Adapter
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
Iteratortrait forInterleave. - The
nextmethod 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
InterleaveExtso you can call.interleave(other_iter)on any iterator.
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]);There are no comments for now.