914-iterator-min-max — Iterator min and max
Tutorial
The Problem
Finding the extreme value in a sequence is ubiquitous: highest score, earliest date, longest string, coldest temperature. The standard approach traverses the sequence once while tracking the running extreme. Rust's Iterator::min() and Iterator::max() encapsulate this idiom, requiring Ord for direct comparison. For types with partial ordering (like f64, which has NaN), min_by and max_by accept a custom comparator. min_by_key and max_by_key extract a sort key from each element. All return Option<T> — returning None for empty iterators rather than panicking.
🎯 Learning Outcomes
.min() and .max() for total-order types (Ord).min_by_key() and .max_by_key() to find extremes by a derived keyf64 using .reduce(f64::min) since f64 is not OrdList.fold_left max and Base.List.min_eltCode Example
// Built into Iterator — no manual fold needed
fn slice_min(nums: &[i32]) -> Option<i32> {
nums.iter().copied().min()
}
fn slice_max(nums: &[i32]) -> Option<i32> {
nums.iter().copied().max()
}
// min/max by derived property — no Ord required on the struct
fn shortest<'a>(words: &[&'a str]) -> Option<&'a str> {
words.iter().copied().min_by_key(|w| w.len())
}Key Differences
Ord for min/max — provides a compile-time guarantee; OCaml's polymorphic max works on any type but may compare incorrectly.f64 doesn't implement Ord (due to NaN); OCaml's polymorphic compare handles NaN with platform-defined behavior.Option<T> for empty input; OCaml List.fold_left requires a non-empty list or explicit check.min_by_key / max_by_key are first-class; OCaml requires min_elt ~compare:(fun a b -> compare (key a) (key b)).OCaml Approach
OCaml uses List.fold_left max x xs where max is the built-in polymorphic comparison. Base.List.min_elt ~compare and Base.List.max_elt ~compare are the idiomatic equivalents with an explicit comparator. Standard OCaml: let max_student students = List.fold_left (fun acc s -> if s.score > acc.score then s else acc) (List.hd students) (List.tl students). OCaml's built-in compare works on all types but is untyped — Ord in Rust provides a type-safe alternative.
Full Source
#![allow(clippy::all)]
//! 275. Finding extremes: min() and max()
//!
//! `min()` and `max()` consume an iterator and return `Option<T>`.
//! They require `Ord` — use `min_by_key` / `max_by_key` for structs,
//! and `reduce(f64::min)` for floats which lack a total ordering.
/// Find the minimum integer in a slice, returning None for empty input.
pub fn slice_min(nums: &[i32]) -> Option<i32> {
nums.iter().copied().min()
}
/// Find the maximum integer in a slice, returning None for empty input.
pub fn slice_max(nums: &[i32]) -> Option<i32> {
nums.iter().copied().max()
}
/// Return the shortest string in a slice by character count.
pub fn shortest<'a>(words: &[&'a str]) -> Option<&'a str> {
words.iter().copied().min_by_key(|w| w.len())
}
/// Return the longest string in a slice by character count.
pub fn longest<'a>(words: &[&'a str]) -> Option<&'a str> {
words.iter().copied().max_by_key(|w| w.len())
}
#[derive(Debug, PartialEq)]
pub struct Student {
pub name: &'static str,
pub score: u32,
}
/// Return the student with the highest score.
pub fn top_student(students: &[Student]) -> Option<&Student> {
students.iter().max_by_key(|s| s.score)
}
/// Return the student with the lowest score.
pub fn bottom_student(students: &[Student]) -> Option<&Student> {
students.iter().min_by_key(|s| s.score)
}
/// Find the minimum of f64 values using reduce, since f64 is not Ord.
/// Returns None for empty input or if any value is NaN.
pub fn float_min(nums: &[f64]) -> Option<f64> {
nums.iter().copied().reduce(f64::min)
}
/// Find the maximum of f64 values using reduce, since f64 is not Ord.
pub fn float_max(nums: &[f64]) -> Option<f64> {
nums.iter().copied().reduce(f64::max)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_min_max_integers() {
let nums = [3i32, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
assert_eq!(slice_min(&nums), Some(1));
assert_eq!(slice_max(&nums), Some(9));
}
#[test]
fn test_empty_returns_none() {
let empty: &[i32] = &[];
assert_eq!(slice_min(empty), None);
assert_eq!(slice_max(empty), None);
}
#[test]
fn test_single_element() {
assert_eq!(slice_min(&[42]), Some(42));
assert_eq!(slice_max(&[42]), Some(42));
}
#[test]
fn test_min_max_by_key_strings() {
let words = ["banana", "apple", "fig", "kiwi", "cherry"];
assert_eq!(shortest(&words), Some("fig"));
// max_by_key returns last of equal elements; "cherry" ties "banana" at len 6
assert_eq!(longest(&words), Some("cherry"));
}
#[test]
fn test_min_max_by_key_struct() {
let students = vec![
Student {
name: "Alice",
score: 95,
},
Student {
name: "Bob",
score: 72,
},
Student {
name: "Carol",
score: 88,
},
];
assert_eq!(top_student(&students).map(|s| s.name), Some("Alice"));
assert_eq!(bottom_student(&students).map(|s| s.name), Some("Bob"));
}
#[test]
fn test_float_min_max() {
let floats = [3.1f64, 1.4, 1.5, 9.2, 2.6];
assert_eq!(float_min(&floats), Some(1.4));
assert_eq!(float_max(&floats), Some(9.2));
}
#[test]
fn test_float_empty() {
assert_eq!(float_min(&[]), None);
assert_eq!(float_max(&[]), None);
}
#[test]
fn test_all_same_values() {
let nums = [7i32, 7, 7, 7];
assert_eq!(slice_min(&nums), Some(7));
assert_eq!(slice_max(&nums), Some(7));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_min_max_integers() {
let nums = [3i32, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
assert_eq!(slice_min(&nums), Some(1));
assert_eq!(slice_max(&nums), Some(9));
}
#[test]
fn test_empty_returns_none() {
let empty: &[i32] = &[];
assert_eq!(slice_min(empty), None);
assert_eq!(slice_max(empty), None);
}
#[test]
fn test_single_element() {
assert_eq!(slice_min(&[42]), Some(42));
assert_eq!(slice_max(&[42]), Some(42));
}
#[test]
fn test_min_max_by_key_strings() {
let words = ["banana", "apple", "fig", "kiwi", "cherry"];
assert_eq!(shortest(&words), Some("fig"));
// max_by_key returns last of equal elements; "cherry" ties "banana" at len 6
assert_eq!(longest(&words), Some("cherry"));
}
#[test]
fn test_min_max_by_key_struct() {
let students = vec![
Student {
name: "Alice",
score: 95,
},
Student {
name: "Bob",
score: 72,
},
Student {
name: "Carol",
score: 88,
},
];
assert_eq!(top_student(&students).map(|s| s.name), Some("Alice"));
assert_eq!(bottom_student(&students).map(|s| s.name), Some("Bob"));
}
#[test]
fn test_float_min_max() {
let floats = [3.1f64, 1.4, 1.5, 9.2, 2.6];
assert_eq!(float_min(&floats), Some(1.4));
assert_eq!(float_max(&floats), Some(9.2));
}
#[test]
fn test_float_empty() {
assert_eq!(float_min(&[]), None);
assert_eq!(float_max(&[]), None);
}
#[test]
fn test_all_same_values() {
let nums = [7i32, 7, 7, 7];
assert_eq!(slice_min(&nums), Some(7));
assert_eq!(slice_max(&nums), Some(7));
}
}
Deep Comparison
OCaml vs Rust: Iterator min() and max()
Side-by-Side Code
OCaml
let list_min = function
| [] -> None
| lst -> Some (List.fold_left min max_int lst)
let list_max = function
| [] -> None
| lst -> Some (List.fold_left max min_int lst)
(* min by derived property *)
let shortest words =
match words with
| [] -> None
| hd :: tl ->
Some (List.fold_left (fun acc w ->
if String.length w < String.length acc then w else acc
) hd tl)
Rust (idiomatic)
// Built into Iterator — no manual fold needed
fn slice_min(nums: &[i32]) -> Option<i32> {
nums.iter().copied().min()
}
fn slice_max(nums: &[i32]) -> Option<i32> {
nums.iter().copied().max()
}
// min/max by derived property — no Ord required on the struct
fn shortest<'a>(words: &[&'a str]) -> Option<&'a str> {
words.iter().copied().min_by_key(|w| w.len())
}
Rust (functional / explicit fold)
fn slice_min_fold(nums: &[i32]) -> Option<i32> {
nums.iter().copied().reduce(|acc, x| if x < acc { x } else { acc })
}
fn slice_max_fold(nums: &[i32]) -> Option<i32> {
nums.iter().copied().reduce(|acc, x| if x > acc { x } else { acc })
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Min of list | val list_min : int list -> int option | fn slice_min(nums: &[i32]) -> Option<i32> |
| Max by key | List.fold_left with manual comparator | Iterator::max_by_key(fn) |
| Float extreme | List.fold_left Float.min | .reduce(f64::min) |
| Empty case | pattern match on [] | Returns None automatically |
Key Insights
List module lacks min/max functions, so idiomatic OCaml uses List.fold_left min max_int lst. Rust's Iterator trait provides .min() and .max() directly, making the intent explicit.Ord requirement and the float problem:** Rust's .min()/.max() require T: Ord — a total ordering. f64 does not implement Ord because NaN breaks total order. The workaround is .reduce(f64::min) which uses f64's built-in partial comparison. OCaml avoids this distinction by having a polymorphic min that uses structural comparison everywhere.min_by_key eliminates boilerplate:** OCaml requires a hand-written fold to find the minimum of a derived property. Rust's .min_by_key(|x| x.score) expresses this directly, without needing to implement Ord on the struct or build intermediate tuples.copied():** Iterating over &[i32] yields &i32 references. .copied() converts them to owned i32 values before calling .min(), keeping the return type Option<i32> rather than Option<&i32>. For non-Copy types, use .cloned() or work with references directly.Option/option, but OCaml requires an explicit pattern match on the empty list before the fold, while Rust's iterator methods handle the empty case internally and return None automatically.When to Use Each Style
**Use .min() / .max()** when your type implements Ord and you want the simplest possible expression.
**Use .min_by_key() / .max_by_key()** when comparing structs by a derived numeric or comparable field.
**Use .reduce(f64::min)** when working with floats, which lack a total ordering and cannot use .min() directly.
**Use explicit fold / reduce** when you need custom tie-breaking logic or are computing several aggregates in one pass.
Exercises
top_n<T: Ord + Clone>(data: &[T], n: usize) -> Vec<T> that returns the n largest elements without sorting the whole slice.argmin<T: PartialOrd>(data: &[T]) -> Option<usize> using enumerate().min_by(...) that returns the index of the minimum.max_by_key.