Skip to content

← Closures and Iterators step 15 of 28

Medium Primitives

scan: a running balance that stops at the floor

Track a running balance and stop the moment it would go under a floor.

pub fn balances(deltas: Vec<i64>, floor: i64) -> Vec<i64>

The balance starts at 0. Apply each delta in turn and emit the balance after that delta. The moment applying a delta would take the balance below floor, stop: that balance is not emitted, and neither is anything after it. This is a clean stop, not an error.

deltas = [10, -3, 5],   floor = 0   ->  [10, 7, 12]
deltas = [10, -20, 5],  floor = 0   ->  [10]          (-10 would breach)
deltas = [-1],          floor = 0   ->  []
deltas = [],            floor = 0   ->  []

Equal to the floor is fine — only strictly below stops it.

scan is fold that yields as it goes

fn scan<St, B, F>(self, initial_state: St, f: F) -> Scan<Self, St, F>
where F: FnMut(&mut St, Self::Item) -> Option<B>;

Read the closure’s type carefully, because both halves are the lesson.

It receives &mut St. Not St, not St returned back. The adapter owns the state and lends it to you mutably on every call. So every touch of the state goes through a dereference:

let next = *state + d;    // read
*state = next;            // write

Forget a * and you get a diagnostic about &mut i64 where an i64 was expected — the starter has both mistakes, and the first one reads

error[E0369]: cannot add `i64` to `&mut i64`

because Add is implemented for &i64 but not for &mut i64. This is the same family of confusion as item 9.7’s &&i64: the reference level is part of the type, and operators do not paper over it.

It returns Option<B>, and None ends the iterator. That is what makes scan the right tool here rather than a hand-rolled loop: “keep going while this holds, then stop” is already in the adapter’s contract. Some(v) yields v; None terminates — permanently, like take_while, not like filter.

And note B is whatever you yield, which need not be St. Here they are both i64 and it is tempting to write Some(state); that yields the reference, and the error lands at the far end of the chain:

error[E0277]: a value of type `Vec<i64>` cannot be built from an iterator
              over elements of type `&mut i64`

A type error inside a closure often does not surface at the closure. It surfaces at collect, which is the first place the compiler has a concrete expectation to compare against. When collect complains about an item type you did not think you produced, walk back up the chain.

Why not fold or try_fold?

Because the answer is the whole prefix, not a single value.

fold returns one accumulator at the end. try_fold returns one accumulator or a short circuit. Neither can yield the intermediate states, so making them work here means pushing into a Vec you carry as the accumulator — which is a fold pretending to be a scan, and loses laziness: the whole input is walked even though the answer stopped at element two.

scan is lazy. Chain a .take(3) after it and only three states are ever computed. That composability is the reason the adapter exists.

Why not a for loop with a mutable local?

You can. It is also what clippy’s needless_range_loop and explicit_counter_loop are usually pushing you away from, and — more importantly — a loop is a statement, not a value. You cannot pass a loop to .take(3), you cannot chain another adapter onto it, and you cannot hand it to a function expecting an iterator. scan gives you the same computation as a value.

The honest counterpoint: for a one-off computation whose result you consume immediately, a loop is often clearer, and nobody should reach for scan to prove a point. Reach for it when the running state is part of a chain.

Detail

scan‘s state is created once, before iteration begins, and lives in the Scan adapter. It is not re-created per element and it is not shared with anything else — which is why &mut is safe here without any of item 9.8’s RefCell machinery.

Grade is compile + tests + clippy -D warnings.

Loading visualization…