Skip to content

← Closures and Iterators step 10 of 28

Medium Primitives

fold and try_fold: a total that refuses to overflow

Two functions over the same numbers.

pub fn checked_total(nums: Vec<i64>) -> Option<i64>
pub fn running_max(nums: Vec<i64>) -> Vec<i64>
  • checked_total — the sum of all the numbers, or None if the running total ever overflows i64. [] is Some(0); [i64::MAX, 1] is None.
  • running_max — for each position, the largest value seen up to and including that position. [3, 1, 4, 1, 5] gives [3, 3, 4, 4, 5]. [] gives []. The output always has the same length as the input.

Why the naive total is wrong, and why nothing tells you

This binary is compiled with -O, which means overflow checks are off. acc + x past i64::MAX does not panic here. It wraps, silently, to a negative number. The starter does exactly that, and one of the visible test cases reports:

expected total: null
actual   total: -9223372036854775808

That is i64::MIN. The arithmetic did not fail — it produced a confidently wrong answer. This is the single best argument for the checked_* family: checked_add returns Option<i64>, None on overflow, in every build profile. A debug build would have panicked and you would have found this in testing; a release build would have shipped it.

The fold family

All five of these are consumers: they run the whole iterator and return a value, not another iterator.

Method Seed Returns Empty input
fold(init, f) you supply B init
reduce(f) first item Option<T> None
sum() T::default() T zero
product() one T one
try_fold(init, f) you supply Option<B> / Result<B, E> Some(init)

fold is the general one. Its signature is fold<B, F: FnMut(B, Self::Item) -> B>(self, init: B, f: F) -> B — you thread an accumulator of any type through every element. The accumulator does not have to be a number. In running_max it is a Vec<i64>, which is a perfectly ordinary thing for it to be and the reason fold is worth knowing properly rather than as “the sum one”.

reduce has no seed — it takes the first element as the accumulator, so it must return Option to describe an empty input. That is the whole difference, and it matters: fold(0, ..) on [] gives 0, reduce(..) on [] gives None. When there is no sensible identity element (the maximum of no numbers is not zero), reduce is the honest choice.

try_fold: folding that can give up

fn try_fold<B, F, R>(&mut self, init: B, f: F) -> R
where F: FnMut(B, Self::Item) -> R, R: Try<Output = B>;

The closure returns something try-ableOption<B> or Result<B, E> — and the moment it returns None or Err, try_fold stops and returns that. It does not visit the remaining elements. It is the short-circuiting fold, and it is the cleanest thing in std for “reduce, but any step may fail”.

Here the closure is exactly |acc, x| acc.checked_add(x), which already has the right shape. That is not a coincidence — checked_add was designed to compose this way.

One wrinkle worth knowing: try_fold takes &mut self, not self, so the iterator survives the call and can be used again — which is how the short-circuiting consumers in item 9.18 are built on top of it.

The starter is rejected by clippy before you even get to the bug

Some(nums.into_iter().fold(0, |acc, x| acc + x))
error: this `.fold` can be written more succinctly using another method
       [clippy::unnecessary_fold]

Default-on, so it is graded. fold(0, |a, b| a + b) is .sum(), fold(1, |a, b| a * b) is .product(), fold(false, |a, b| a || b) is .any(), fold(true, ..) is .all(). Clippy knows all four.

And note that you cannot fix checked_total with .sum(). .sum() uses plain + and wraps exactly like the fold did. The overflow requirement is what forces try_fold; there is no shortcut method that returns Option.

.sum() and the turbofish

While you are here: nums.iter().sum() on its own is

error[E0282]: type annotations needed

because Sum is implemented for many types and nothing says which one you want. let t: i64 = nums.iter().sum(); or nums.iter().sum::<i64>(). Same return-type-driven inference as parse in item 9.9.

running_max

Write it with fold and a Vec<i64> accumulator: each step looks at the last element pushed so far, takes the larger of that and the current value, and pushes it. Option::map_or handles the empty-so-far case in one expression. (Item 9.15 shows the lazier scan-based version — but scan is a different tool and this problem wants the fold.)

Grade is compile + tests + clippy -D warnings.

Loading visualization…