276: Custom Comparison with min_by() and max_by()
Functional Programming
Tutorial
The Problem
Many types either cannot implement Ord (floating-point numbers have NaN) or require a non-standard ordering (reverse order, case-insensitive comparison, comparison by a computed property). The min_by() and max_by() methods accept a custom comparator function Fn(&A, &A) -> Ordering, enabling arbitrary orderings without modifying the type's own comparison. This is essential for types like f64 and for domain-specific orderings.
🎯 Learning Outcomes
min_by(cmp) and max_by(cmp) as taking explicit Fn(&A, &A) -> Ordering comparatorspartial_cmp for floating-point types that implement PartialOrd but not Ordmin_by with a reversed comparator to implement max-as-min (useful for min-heaps)min_by (comparator function) from min_by_key (key extraction function)Code Example
#![allow(clippy::all)]
//! 276. Custom comparison min_by() and max_by()
//!
//! `min_by(cmp)` and `max_by(cmp)` take a `Fn(&A, &A) -> Ordering` comparator.
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
#[test]
fn test_min_by_float() {
let floats = [3.0f64, 1.0, 2.0];
let min = floats
.iter()
.copied()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
assert_eq!(min, Some(1.0));
}
#[test]
fn test_max_by_reversed() {
let nums = [1i32, 5, 3, 2, 4];
let max = nums.iter().min_by(|a, b| b.cmp(a));
assert_eq!(max, Some(&5));
}
#[test]
fn test_min_by_multi_key() {
let words = ["bb", "aa", "c"];
let min = words
.iter()
.min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
assert_eq!(min, Some(&"c"));
}
}Key Differences
Ord for f64 — min_by with partial_cmp is the required pattern; OCaml's polymorphic compare handles floats but NaN behavior differs.&A references in the comparator; OCaml's functions typically take values by value.min_by_key(f) is min_by(|a,b| f(a).cmp(&f(b))) — prefer min_by_key when the key is cheap to compute.min_by returns the first encountered minimum; this is consistent with Rust's general iterator stability.OCaml Approach
OCaml's List.fold_left with a custom comparison function serves the same purpose. The compare parameter accepts any 'a -> 'a -> int-typed function:
let min_by cmp lst =
List.fold_left (fun acc x -> if cmp x acc < 0 then x else acc) (List.hd lst) lst
let min_float = min_by Float.compare [3.0; 1.0; 2.0] (* 1.0 *)
OCaml's compare function is polymorphic by default, handling floats (including NaN) with structural comparison.
Full Source
#![allow(clippy::all)]
//! 276. Custom comparison min_by() and max_by()
//!
//! `min_by(cmp)` and `max_by(cmp)` take a `Fn(&A, &A) -> Ordering` comparator.
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
#[test]
fn test_min_by_float() {
let floats = [3.0f64, 1.0, 2.0];
let min = floats
.iter()
.copied()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
assert_eq!(min, Some(1.0));
}
#[test]
fn test_max_by_reversed() {
let nums = [1i32, 5, 3, 2, 4];
let max = nums.iter().min_by(|a, b| b.cmp(a));
assert_eq!(max, Some(&5));
}
#[test]
fn test_min_by_multi_key() {
let words = ["bb", "aa", "c"];
let min = words
.iter()
.min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
assert_eq!(min, Some(&"c"));
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
#[test]
fn test_min_by_float() {
let floats = [3.0f64, 1.0, 2.0];
let min = floats
.iter()
.copied()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
assert_eq!(min, Some(1.0));
}
#[test]
fn test_max_by_reversed() {
let nums = [1i32, 5, 3, 2, 4];
let max = nums.iter().min_by(|a, b| b.cmp(a));
assert_eq!(max, Some(&5));
}
#[test]
fn test_min_by_multi_key() {
let words = ["bb", "aa", "c"];
let min = words
.iter()
.min_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
assert_eq!(min, Some(&"c"));
}
}
Exercises
min_by(|a, b| a.len().cmp(&b.len())) and verify it gives the same result as min_by_key(|s| s.len()).min_by with |a, b| a.abs().partial_cmp(&b.abs()).min_by_key_or_first function that returns the first element if the slice is empty or returns the minimum-by-key otherwise.