Skip to content

← Closures and Iterators step 23 of 28

Hard Framework

Write your own adapters, with an extension trait

Two adapters of your own, reachable by method call on any iterator.

pub struct DedupAdjacent<I: Iterator> { /* .. */ }
pub struct Pairwise<I: Iterator>      { /* .. */ }

pub trait IterExt: Iterator + Sized {
    fn dedup_adjacent(self) -> DedupAdjacent<Self> { /* .. */ }
    fn pairwise(self) -> Pairwise<Self> { /* .. */ }
}
impl<I: Iterator> IterExt for I {}

pub fn dedup_and_pair(nums: Vec<i64>) -> (Vec<i64>, Vec<(i64, i64)>)
  • dedup_adjacent drops any item equal to the one just emitted. [1, 1, 2, 2, 2, 3] becomes 1, 2, 3; [1, 2, 1] stays 1, 2, 1 — it is adjacent dedup, not global dedup.
  • pairwise yields every adjacent pair: [1, 2, 3] becomes (1, 2), (2, 3). It is slice::windows(2) at the iterator level, so an input of n items yields n - 1 pairs, and fewer than two items yields nothing.

dedup_and_pair returns the deduplicated numbers and the pairs of the deduplicated numbers.

Both adapters must implement size_hint correctly, and the tests check it — including that Pairwise‘s hint is exact when its source is exact.

Why this is the most transferable pattern in the track

Every adapter in std is exactly this shape, and you have been reading their names in error messages for the whole track:

Map<Filter<Iter<'_, i32>, {closure}>, {closure}>

That nest is not a wrapper hierarchy that costs a virtual call per level. Each layer is a plain struct holding the layer below, each next is a small generic function, and monomorphisation plus inlining collapses the whole tower into one loop. Building your own tells you that from the inside.

The extension trait, and the two bounds you cannot skip

You cannot add a method to Iterator — it is not your trait. What you can do is declare a new trait with default method bodies and blanket-implement it for everything:

pub trait IterExt: Iterator + Sized { /* default bodies */ }
impl<I: Iterator> IterExt for I {}

The empty impl block is the whole implementation: every method already has a body in the trait, so every iterator in the universe gets them for free. This is how itertools works, and how you should ship any “I wish Iterator had X” helper.

The starter omits the supertrait bounds, and both halves of the resulting error matter:

error[E0277]: `Self` is not an iterator
error[E0277]: the size for values of type `Self` cannot be known at compilation time
  • : Iterator — the default bodies store self in a struct whose impls require I: Iterator. Without the supertrait, Self is just some type.
  • : SizedSelf in a trait is ?Sized by default, and DedupAdjacent { inner: self, .. } has to put self in a field by value, which needs a known size. Every extension trait taking self by value needs this.

Where the Clone bound goes — the real design decision

Pairwise must yield each item twice: once as the right half of one pair and once as the left half of the next. So it has to keep a copy, which means Item: Clone. DedupAdjacent has to remember the last item it emitted to compare against, so it needs Item: PartialEq + Clone too.

The question is where to write those bounds, and the answer is worth internalising:

// On the struct — infects every use, even ones that never call next().
pub struct Pairwise<I: Iterator> where I::Item: Clone { .. }

// On the impl — the bound is required only to iterate. Prefer this.
impl<I> Iterator for Pairwise<I> where I: Iterator, I::Item: Clone { .. }

Put a bound on the struct and it propagates to every function that so much as names the type. Put it on the impl and it is demanded only where it is actually needed. std follows the second rule almost everywhere; the struct here carries I: Iterator only because the field type Option<I::Item> mentions the associated type and cannot be written without it.

The hints

Get these right; they are checked.

  • DedupAdjacent: every remaining item could be a duplicate of the last one you emitted, so the lower bound collapses to 0. The upper bound is unchanged — you never emit more than the source has left. So (0, inner_upper). This is exactly why filter reports (0, Some(n)) in item 9.22’s table.
  • Pairwise: pairs remaining is (items remaining, plus one if you are still holding the previous item) minus one — floored at zero. saturating_sub is the right tool; a plain - 1 on usize underflows on an empty source and, with -O, wraps to a colossal number instead of panicking.

type_complexity

Once adapters nest, clippy’s default-on type_complexity starts objecting to the types you have to write down. Two fixes, both good: a type alias, or returning impl Iterator<Item = T> from functions that build a chain — which hides the tower entirely and lets you change it without touching callers.

A note on the future

Rust has gen blocks in the pipeline, which would let you write both of these as a few lines of imperative code with yield. As of 1.95 they are nightly-only, so on stable — and in this course — the struct is the way. Knowing the struct form is not wasted: gen desugars to one.

Grade is compile + tests + clippy -D warnings.

Loading visualization…