Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-209 · Case file with fixtures · Case 181 of 694 · Runtime evidence

Iterator::max_by_key Returns the Last Equal Maximum

Rust's max_by_key deliberately returns the last element among equal maxima. If first-wins, stable identity, or another tie rule matters, encode it in the comparison or reduction instead of relying on incidental iteration order.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The iterator contract deliberately resolves equal maxima in favor of the last element, so encounter order becomes an implicit tie-breaker.
First discriminating check
Place two labeled candidates at the same maximum with a lower value between them, then record which label max_by_key returns.

I selected the highest-scoring candidate with max_by_key and expected an earlier equal candidate to remain the winner. A later candidate with the same score replaced it.

The failing program uses labels first, middle, and last, with both outer candidates scoring ten. Rust returns last.

The final equal maximum wins by documented design.

Equal maxima are resolved by encounter order

Iterator::max_by_key returns the element producing the greatest key. Its documentation explicitly states that when several elements are equally maximum, the last one is returned.

Conceptually, its update rule behaves like:

replace best when candidate_key >= best_key

A first-wins maximum would replace only on strict greater-than.

The repaired program uses an explicit reduce and keeps best when scores are equal. The code now states its tie policy.

“Last” depends on the iterator

For a vector or slice, encounter order is stable and visible. For a HashMap, iteration order is arbitrary and can vary with hasher state or map history. Last-wins over an unordered source is not a stable business decision.

If I select a server, offer, leader, or retry candidate from a hash map, I add a deterministic secondary key or collect into a stable order first. A passing test from one process does not establish reproducibility.

Parallel iterators or distributed reductions may have different combination orders. I treat tie-breaking as part of the reduction algebra, not as a cosmetic detail after finding the score.

Put the full order in the key when possible

For a deterministic preference such as highest score then smallest ID, I can compare a composite key with the direction of each component chosen carefully.

Tuples use lexicographic ordering. If both components use normal maximum order, (score, id) chooses the largest ID on a score tie. To choose the smallest ID, I can use Reverse(id) for the second component or write an explicit comparator.

I prefer a named comparator when tuple direction becomes hard to read. A comment such as “highest health, then earliest registration” is valuable, but a regression test with an actual tie is stronger.

max_by has the same equal-result direction

Iterator::max_by lets me provide a comparator, but equal comparisons still follow the method's documented tie behaviour. Returning Ordering::Equal is a real policy decision.

If I want the comparator itself to decide every tie, it must continue comparing stable fields until two records are genuinely interchangeable. If records remain equal under the full domain order, choosing either should be harmless.

This is why I avoid comparators that look only at a rounded score when identity matters. Rounding can create many accidental ties.

Reversing the iterator reverses the tie result

Calling .rev().max_by_key(...) on a double-ended iterator can make the original first maximum become the last maximum in reversed encounter order. This is concise but easy to misunderstand and only applies when reversal is available.

The explicit reduce in the fixture is longer but exposes the strict comparison. In shared code I may extract a named first_max_by_key helper with tests if this policy appears often.

I do not depend on clever reversal for a critical selection algorithm unless the name communicates it.

Minima are worth checking separately

Rust's minimum operations do not necessarily mirror every intuition about maximum tie behaviour. I read the exact method documentation rather than deriving one contract from another.

More generally, sort stability, heap ordering, binary search among duplicates, selection algorithms, and extrema all have their own rules for equal values. “They compare equal” does not mean “the first representation is preserved.”

The Atlas connects these cases because hidden representative choice is a frequent source of nondeterministic-looking output.

Selection policy can affect fairness

In a scheduler, last-wins can systematically prefer recently encountered candidates. First-wins can systematically prefer old ordering. A deterministic ID rule can concentrate load on one candidate. Random tie-breaking can improve distribution but complicates reproducibility.

The standard method gives a mechanical rule, not a fairness guarantee. I decide fairness at the application level and measure it over time.

For load balancing I may rotate the starting point, track recent assignments, or sample among equal maxima. Those are stateful policies and should not be hidden inside a one-line maximum call.

My regression data must contain ties

A test with unique scores cannot detect the entire bug. I place equal maxima before and after a lower candidate so both the maximum value and identity are visible.

I also test empty input because the return type is Option, one item, adjacent ties, and sources with unstable iteration order. For composite keys I test each tie-break level separately.

The core principle is that choosing a maximum includes choosing a representative among equals. max_by_key chooses the last encountered maximum. If that identity carries business meaning, I encode a complete order or a named reduction so the decision is deliberate.