Skip to content

← Closures and Iterators step 8 of 28

Easy Primitives

Laziness: prove the pipeline interleaves

Instrument an iterator chain and return the log of what actually ran, in the order it actually ran.

pub fn trace(nums: Vec<i64>) -> Vec<String>

Build exactly this pipeline over nums:

nums.iter().map(double).filter(divisible_by_four).take(3)

where

  • the map closure pushes "map:{x}"x is the input, before doubling — then returns x * 2;
  • the filter closure pushes "filter:{y}"y is the value it is testing, i.e. the already-doubled number — then returns y % 4 == 0.

Consume the pipeline and count how many items came out. Return the log, with one final entry "kept:{n}" appended, where n is that count.

For nums = [2] the answer is ["map:2", "filter:4", "kept:1"]. For nums = [] it is ["kept:0"].

What this problem is actually testing

Run it on [1, 2, 3, 4, 5, 6] and look at the shape of the log:

map:1  filter:2  map:2  filter:4  map:3  filter:6  ...

Not map:1 map:2 map:3 map:4 map:5 map:6 followed by six filters. The two closures interleave, one element at a time, all the way through the chain, and then the next element starts.

This is the single most important structural fact about Rust iterators and it is invisible until you instrument one. map does not build a new collection. filter does not build a new collection. nums.iter().map(f) allocates nothing, calls f zero times, and produces a value of type Map<Iter<'_, i64>, F> — a little struct holding the source iterator and your closure, and nothing else.

Work happens only when something calls next(). And when it does, the request travels backwards along the chain: Take::next asks Filter::next, which asks Map::next, which asks Iter::next, which finally produces a number — which then flows forward through f, then through the predicate, then out. One element, one round trip.

The consequence the hidden cases check

Try [2, 4, 6, 8, 10] on paper before you run it.

2 → 4 passes. 4 → 8 passes. 6 → 12 passes. That is three, and take(3) is satisfied. 8 and 10 are never doubled and never tested. They do not appear in the log at all.

An eager language — one where map returns a list — must touch all five. Rust touches three, because nothing ever asked for a fourth. That is why chaining ten adapters costs one pass, not ten, and why take in the middle of a chain is a real optimisation rather than a filter applied at the end.

The two warnings you will meet in the first minute

The starter builds the chain and never consumes it. Both of these are warn-by-default in rustc, and this gate runs -D warnings, so both are hard failures:

warning: unused `std::iter::Take` that must be used
  = note: iterators are lazy and do nothing unless consumed
help: use `let _ = ...` to ignore the resulting value
warning: `Iterator::map` call that discard the iterator's values
  | called `Iterator::map` with callable that returns `()`
  | after this call to map, the resulting iterator is `impl Iterator<Item = ()>`,
  | which means the only information carried by the iterator is the number of items
  = note: `#[warn(map_unit_fn)]` (part of `#[warn(unused)]`) on by default

The compiler wrote “iterators are lazy and do nothing unless consumed” for you. It is not a style note; it is a bug report.

Do not take the help: suggestion. let _ = nums.iter().map(..) silences the warning and still runs nothing — you would have converted a loud bug into a quiet one. That is why the log has a "kept:n" entry: a solution that never consumes the pipeline cannot produce it.

The correct fix is to end the chain with something that actually drives it. count() is the obvious choice here since you need the number anyway. Others: collect(), sum(), for_each(), last(), or a for loop.

Two closures, one log

Both closures need to append to the same Vec<String>. Your first instinct — capture &mut log in both — does not work: two closures alive at the same time cannot both hold an exclusive borrow of the same place (E0499). That is the aliasing rule, not an iterator quirk.

The clean answer here is RefCell<Vec<String>>: shared borrows for both closures, exclusive access checked at runtime when you call borrow_mut(). It cannot panic in this program because the two borrow_mut()s never overlap — map‘s guard is dropped at the end of its statement, long before filter runs. Recover the Vec at the end with into_inner().

This is a legitimate use of RefCell and worth recognising as one: two closures genuinely need to share one piece of mutable state, and there is no ordering you could impose that would let a single &mut serve both. Item 9.27 covers the case where restructuring is the better answer — and it is the more common case.

Small details that will bite

  • filter‘s predicate receives &Item. Since map yields i64, that is &i64 — destructure it with |&y|. Item 9.7 is the full story.
  • Log the map closure’s input, not its output.
  • -4 % 4 is 0 in Rust; remainder takes the sign of the dividend, so negative multiples of four still pass.

Grade is compile + tests + clippy -D warnings.

Loading visualization…