Finding the “best” item in a collection — longest string, heaviest order, latest timestamp — is one of those tasks where a hand-rolled fold keeps showing up. Iterator::max_by_key does the same job in one call, and min_by_key is right there next to it.
The fold you keep writing
You have a slice of strings and want the longest one. The DIY version looks something like this:
1
2
3
4
5
6
7
8
9
| let words = ["pear", "raspberry", "fig", "kiwi"];
let longest = words.iter().fold(None, |best, w| match best {
None => Some(w),
Some(b) if w.len() > b.len() => Some(w),
other => other,
});
assert_eq!(longest, Some(&"raspberry"));
|
That’s a lot of code for a one-liner concept. max_by_key collapses the whole pattern:
1
2
3
| let words = ["pear", "raspberry", "fig", "kiwi"];
let longest = words.iter().max_by_key(|w| w.len());
assert_eq!(longest, Some(&"raspberry"));
|
You hand it a closure that extracts the key — the thing you want to maximise — and it returns the item that produced the largest key.
Works on anything Ord
The key doesn’t have to be a number. It just has to implement Ord, which means strings, tuples, dates, your own types — anything totally ordered:
1
2
3
4
5
6
7
8
9
10
11
| #[derive(Debug, PartialEq)]
struct Order { id: u32, total_cents: u64 }
let orders = vec![
Order { id: 1, total_cents: 1299 },
Order { id: 2, total_cents: 4500 },
Order { id: 3, total_cents: 800 },
];
let biggest = orders.iter().max_by_key(|o| o.total_cents);
assert_eq!(biggest, Some(&Order { id: 2, total_cents: 4500 }));
|
Tuples are where this really shines — you get multi-key sorting for free, because (A, B): Ord compares lexicographically:
1
2
3
4
5
| let words = ["pear", "fig", "kiwi", "lime"];
// Longest word, then alphabetically last among ties.
let pick = words.iter().max_by_key(|w| (w.len(), *w));
assert_eq!(pick, Some(&"pear"));
|
Same trick with min_by_key — the entire _by_key family follows the same shape.
The tie-break rule, and why it matters
When two elements produce equal keys, max_by_key returns the last one and min_by_key returns the first. That asymmetry is in the docs and it bites people:
1
2
3
4
5
6
7
| let nums = [3, 1, 4, 1, 5, 9, 2, 6, 5];
let max = nums.iter().max_by_key(|&&n| n);
let min = nums.iter().min_by_key(|&&n| n);
assert_eq!(max, Some(&9));
assert_eq!(min, Some(&1)); // the first 1, not the second
|
If you need a specific tie-break — “longest word, but earliest in the list when tied” — encode it in the key itself with a tuple, instead of relying on iteration order.
Floats need min_by / max_by
f32 and f64 don’t implement Ord (because NaN), so max_by_key won’t compile if your key is a float. Reach for max_by and pass a comparator instead:
1
2
3
4
5
6
| let measurements = [1.2_f64, 3.4, 0.5, 2.8];
let biggest = measurements.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(biggest, Some(&3.4));
|
Or wrap your floats in something like ordered_float::OrderedFloat and stay with max_by_key.
Takeaway
Any time you find yourself folding to track the “best so far,” check whether max_by_key or min_by_key says it in one line. The closure extracts the score; the iterator returns the winner. For ties you control the rule by shaping the key.