ExamplesBy LevelBy TopicLearning Paths
914 Intermediate

914-iterator-min-max — Iterator min and max

Functional Programming

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

  • • Use .min() and .max() for total-order types (Ord)
  • • Use .min_by_key() and .max_by_key() to find extremes by a derived key
  • • Handle f64 using .reduce(f64::min) since f64 is not Ord
  • • Apply min/max to structs by key extraction (top student, shortest word)
  • • Compare with OCaml's List.fold_left max and Base.List.min_elt
  • Code 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

  • Type safety: Rust requires Ord for min/max — provides a compile-time guarantee; OCaml's polymorphic max works on any type but may compare incorrectly.
  • f64 handling: Rust f64 doesn't implement Ord (due to NaN); OCaml's polymorphic compare handles NaN with platform-defined behavior.
  • Option return: Rust min/max return Option<T> for empty input; OCaml List.fold_left requires a non-empty list or explicit check.
  • key extraction: 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));
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Min of listval list_min : int list -> int optionfn slice_min(nums: &[i32]) -> Option<i32>
    Max by keyList.fold_left with manual comparatorIterator::max_by_key(fn)
    Float extremeList.fold_left Float.min.reduce(f64::min)
    Empty casepattern match on []Returns None automatically

    Key Insights

  • Built-in vs manual fold: OCaml's 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.
  • **Ownership and 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.
  • Empty case handling: Both languages wrap the result in 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

  • Write top_n<T: Ord + Clone>(data: &[T], n: usize) -> Vec<T> that returns the n largest elements without sorting the whole slice.
  • Implement argmin<T: PartialOrd>(data: &[T]) -> Option<usize> using enumerate().min_by(...) that returns the index of the minimum.
  • Find the word with the most distinct characters in a list of strings using max_by_key.
  • Open Source Repos