Skip to content

← Closures and Iterators step 15 of 28

Medium Primitives

scan: a running balance that stops at the floor

Produce the running balances for a sequence of changes, stopping before a balance would fall below the allowed minimum.

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

Start at zero and apply the deltas from left to right. After each addition, append the new balance if it is at least floor. Otherwise, stop and return the balances already collected. Do not include the rejected balance or process later deltas, even if they would raise it again.

Also stop if an addition cannot be represented as an i64. Use checked addition so this behaves consistently across build settings. Overflow does not append a wrapped or saturated value.

Deltas Floor Output
[10, -3, 5] 0 [10, 7, 12]
[10, -20, 5] 0 [10]
[-1] 0 []
[0, 0] 0 [0, 0]
[5, -1] 5 [5]
[i64::MAX, 1] 0 [i64::MAX]

An empty input returns an empty vector. The initial zero is not emitted or checked against the floor; only balances after applying a delta are checked. Landing exactly on the floor is allowed.

Keep state between iterator steps

Use scan to practice an iterator that updates state while producing values. Its closure receives &mut St for the stored state and one input item, then returns Option<B> for the next output. In this exercise, both the state and output value are i64.

Start the adapter with 0i64. Inside the closure, state is an &mut i64:

let next = state.checked_add(d)?;

Method calls can automatically dereference the reference. Here ? returns None from the closure if addition overflows. This makes the collecting operation stop while retaining the balances it has already collected.

On a successful addition that meets the floor, update the stored value with *state = next, then yield Some(next). The * in the assignment writes through the mutable reference. Assigning state = next would try to replace a reference with an integer instead.

The starter also writes state + d. That operator does not automatically convert &mut i64 into the required integer operand. Reading with *state would fix that type error, but ordinary addition still needs to be replaced to handle overflow correctly.

Yield the integer, not Some(state). The output is a Vec<i64>, and the temporary mutable borrow of the adapter's state cannot become its stream of output values. If collect reports an unexpected item type, trace that type back to what the closure returns.

None stops this collection, not every future call

Returning Some(next) makes the scan yield a balance. Returning None makes that call to next() return None, and collect::<Vec<_>>() stops there. This gives the requested prefix.

A scan is not automatically fused: if you keep it and manually call next() again after a None, its closure can run again and yield another value. For example:

let mut steps = [1, 2, 3].into_iter().scan((), |_, n| {
    if n == 2 { None } else { Some(n) }
});
assert_eq!(steps.next(), Some(1));
assert_eq!(steps.next(), None);
assert_eq!(steps.next(), Some(3));

Use .fuse() if a reusable iterator must keep returning None after the first end signal. This function immediately consumes the scan with collect, so it does not resume after a rejected delta.

Compare the available approaches

scan exposes intermediate outputs as an iterator. It does no per-item work until consumed. Adding .take(3) before collection limits this pipeline to at most three scan steps; an earlier stop may produce fewer balances.

fold accumulates a final value. That value can include a vector of running balances, but a normal fold over the full input does not stop early just because its accumulator records a rejected delta. try_fold can stop early and can also carry a vector; it is a valid alternative with a different API.

A direct loop with a mutable balance and break can also implement this behavior clearly. The advantage of scan here is practicing stateful composition with other adapters, not avoiding a loop that Clippy inherently rejects. Output tests alone do not enforce the choice of adapter.

The adapter stores its state between calls and lends it to the closure mutably for each step. Here that state is an owned integer. Other state types can contain references or shared handles; using scan does not imply that everything reachable through its state is unshared.

Passing requires compilation, the expected results and Clippy with warnings treated as errors.