We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Closures and Iterators step 8 of 28
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
mapclosure pushes"map:{x}"—xis the input, before doubling — then returnsx * 2; -
the
filterclosure pushes"filter:{y}"—yis the value it is testing, i.e. the already-doubled number — then returnsy % 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. Sincemapyieldsi64, that is&i64— destructure it with|&y|. Item 9.7 is the full story. -
Log the
mapclosure’s input, not its output. -
-4 % 4is0in Rust; remainder takes the sign of the dividend, so negative multiples of four still pass.
Grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.