Skip to content

← Closures and Iterators step 19 of 28

Medium Primitives

min and max: the tie-breaking rule nobody reads

Find the lowest-scoring and highest-scoring entry.

pub fn extremes(entries: Vec<(String, i64)>) -> (Option<String>, Option<String>)

Return the name of the entry with the smallest score and the name of the entry with the largest score. Empty input gives (None, None).

Ties are the entire problem. Resolve them exactly the way std does — which is not symmetric, and which you almost certainly cannot guess.

The rule

From the standard library documentation, verbatim:

Iterator::maxIf several elements are equally maximum, the last element is returned.

Iterator::minIf several elements are equally minimum, the first element is returned.

Read it twice. max keeps the last. min keeps the first. The same asymmetry applies to every variant — max_by, max_by_key, min_by, min_by_key.

Verified on rustc 1.95:

let v = [("a", 2), ("b", 5), ("c", 2), ("d", 5)];
v.iter().min_by_key(|(_, s)| *s)   // Some(("a", 2))   <- first 2
v.iter().max_by_key(|(_, s)| *s)   // Some(("d", 5))   <- last 5

Why it is asymmetric

It is not an accident and it is not a bug. Both are folds that compare the running best against each new element, and they use complementary comparisons:

  • min replaces only when the new element is strictly less than the current best. An equal element loses, so the first one survives.
  • max replaces when the new element is greater than or equal to the current best. An equal element wins, so the last one survives.

Those two choices are complementary on purpose. Take any two-element sequence [a, b] where a and b compare equal: min returns a and max returns b. They pick different elements rather than both picking the same one — which is what you want if you are using the pair to split a sequence, and what a stable sort would do. The point is not that you must agree with the choice; it is that std specifies it, so there is a right answer, and your reimplementation can be measured against it.

Why this problem exists

A hand-rolled loop gets this wrong with no compile error, no clippy warning, and no failure on any input where the scores happen to be distinct. The starter is exactly such a loop: it is correct on min and wrong on max, and it fails only on the tie cases.

This is the kind of specified-but-unread behaviour that turns into a bug report eighteen months later, when someone’s leaderboard starts naming a different winner than the one on the dashboard. When two implementations of “the maximum” disagree, they disagree about ties, and one of them is not std.

The lesson generalises: do not reimplement a standard-library consumer. min_by_key and max_by_key are one line each, are already correct, and already document their behaviour.

The _by_key variants

fn max_by_key<B: Ord, F: FnMut(&Self::Item) -> B>(self, f: F) -> Option<Self::Item>

f extracts a comparison key; the element comes back, not the key. Two things worth knowing:

  • The key closure is called on every elementmax_by_key is not smart enough to cache it, so if computing the key is expensive, hoist it (.map(|x| (key(x), x)).max()) rather than paying twice per comparison.
  • If you want to compare with a custom ordering rather than a key, that is max_by, which takes a full FnMut(&Item, &Item) -> Ordering.

Clippy will also push you off sort when you only wanted an extreme: sorting to take the first element is O(n log n) for an O(n) question, and unnecessary_sort_by catches some spellings of it.

f64 has no min/max here

Worth knowing before it surprises you: Iterator::min and Iterator::max require Item: Ord, and f64 is not Ord — because NaN is not comparable to anything, including itself. So [1.0f64, 2.0].iter().max() does not compile. Use fold(f64::NEG_INFINITY, f64::max) if you are happy with NaN being ignored, or max_by(|a, b| a.partial_cmp(b).unwrap()) if you would rather it panicked, or total_cmp if you want the IEEE total order. There is no free lunch; you have to say what NaN means to you.

Grade is compile + tests + clippy -D warnings.

Loading visualization…