ExamplesBy LevelBy TopicLearning Paths
060 Fundamental

Option Type — Safe List Maximum

Error Handling

Tutorial Video

Text description (accessibility)

This video demonstrates the "Option Type — Safe List Maximum" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Error Handling. Implement `list_max` that returns the maximum element of a list wrapped in `Option`/`Some`, returning `None` for empty lists instead of raising an exception. Key difference from OCaml: 1. **Almost identical:** `Option<T>` in Rust ≈ `'a option` in OCaml — same constructors, same philosophy

Tutorial

The Problem

Implement list_max that returns the maximum element of a list wrapped in Option/Some, returning None for empty lists instead of raising an exception. Also implement safe_head and option_map.

🎯 Learning Outcomes

  • • Map OCaml's option type to Rust's Option<T> (they're nearly identical)
  • • Use pattern matching on Some/None in both languages
  • • Chain Option operations with map instead of nested matching
  • • Compare recursive and iterator-based approaches to optional results
  • • See how both languages eliminate null pointer errors at the type level
  • 🦀 The Rust Way

  • Idiomatic: iter().copied().max(), first().copied(), Option::map
  • Recursive: Slice pattern matching [head, tail @ ..] mirroring OCaml's h :: t
  • Fold-based: split_first() + fold for iterative maximum
  • Code Example

    pub fn list_max_idiomatic(xs: &[i32]) -> Option<i32> {
        xs.iter().copied().max()
    }
    
    pub fn safe_head_idiomatic(xs: &[i32]) -> Option<i32> {
        xs.first().copied()
    }
    
    pub fn double_max_idiomatic(xs: &[i32]) -> Option<i32> {
        list_max_idiomatic(xs).map(|x| x * 2)
    }

    Key Differences

  • Almost identical: Option<T> in Rust ≈ 'a option in OCaml — same constructors, same philosophy
  • Method richness: Rust's Option has 40+ methods (map, and_then, unwrap_or, ? operator); OCaml's is simpler
  • **The ? operator:** Rust can propagate None with ? (like split_first()?); OCaml uses match or Option.bind
  • Copying: Rust needs .copied() to go from Option<&T> to Option<T>; OCaml doesn't distinguish
  • Iterator integration: Rust's max() returns Option natively — no need for a custom function
  • Empty collection safety: max() on an empty iterator returns None — safe. OCaml's List.fold_left max min_int [] would return min_int — not safe if the list might be legitimately empty vs accidentally empty.
  • **Option as safe max:** iter.max() returns Option<T> — the caller must decide what to do with an empty collection. This forces correct handling instead of silently returning a sentinel.
  • **Iterator::max() is lazy:** It processes elements one by one — no intermediate allocation. OCaml's List.fold_left (fun acc x -> max acc x) (List.hd list) (List.tl list) is equivalent but more verbose.
  • Custom max by key: iter.max_by_key(|x| x.score) finds the maximum by a derived key. OCaml: List.fold_left (fun acc x -> if x.score > acc.score then x else acc) first rest.
  • OCaml Approach

    OCaml's option type (Some x | None) is the idiomatic way to represent partial functions. Pattern matching makes handling both cases explicit and compiler-checked.

    Full Source

    #![allow(clippy::all)]
    //! # Option Type — Safe List Maximum
    //!
    //! OCaml's `option` type maps directly to Rust's `Option<T>`.
    //! Both use `Some`/`None` variants to represent presence/absence of a value,
    //! avoiding null pointer exceptions entirely.
    
    // ---------------------------------------------------------------------------
    // Approach A: Idiomatic Rust — iterator methods
    // ---------------------------------------------------------------------------
    
    /// Returns the maximum element, or `None` if the slice is empty.
    ///
    /// `iter().max()` returns `Option<&T>` — we use `.copied()` to get `Option<T>`
    /// for `Copy` types, avoiding the reference indirection.
    pub fn list_max_idiomatic(xs: &[i32]) -> Option<i32> {
        xs.iter().copied().max()
    }
    
    /// Safe head — returns the first element or `None`.
    ///
    /// Rust's slice method `.first()` returns `Option<&T>`.
    pub fn safe_head_idiomatic(xs: &[i32]) -> Option<i32> {
        xs.first().copied()
    }
    
    /// Map over an Option — `Option::map` is built into Rust's stdlib.
    pub fn double_max_idiomatic(xs: &[i32]) -> Option<i32> {
        list_max_idiomatic(xs).map(|x| x * 2)
    }
    
    // ---------------------------------------------------------------------------
    // Approach B: Functional / recursive — mirrors OCaml closely
    // ---------------------------------------------------------------------------
    
    /// Recursive list_max mirroring OCaml's pattern matching version.
    ///
    /// Uses slice patterns `[head, tail @ ..]` which correspond to
    /// OCaml's `h :: t` destructuring.
    pub fn list_max_recursive(xs: &[i32]) -> Option<i32> {
        match xs {
            [] => None,
            [head, tail @ ..] => match list_max_recursive(tail) {
                None => Some(*head),
                Some(m) => Some(if *head > m { *head } else { m }),
            },
        }
    }
    
    /// Safe head using slice pattern matching.
    pub fn safe_head_recursive(xs: &[i32]) -> Option<i32> {
        match xs {
            [] => None,
            [h, ..] => Some(*h),
        }
    }
    
    /// Manual option_map mirroring OCaml's version.
    ///
    /// In practice you'd always use `Option::map`, but this shows
    /// the pattern matching equivalent.
    #[allow(clippy::manual_map)] // Pedagogical: showing the pattern matching equivalent of Option::map
    pub fn option_map<T, U>(f: impl FnOnce(T) -> U, opt: Option<T>) -> Option<U> {
        match opt {
            None => None,
            Some(x) => Some(f(x)),
        }
    }
    
    // ---------------------------------------------------------------------------
    // Approach C: fold-based — using fold to find maximum
    // ---------------------------------------------------------------------------
    
    /// Maximum via fold — processes left to right with an accumulator.
    pub fn list_max_fold(xs: &[i32]) -> Option<i32> {
        let (&first, rest) = xs.split_first()?;
        Some(rest.iter().fold(first, |acc, &x| acc.max(x)))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_max_basic() {
            let xs = [3, 1, 4, 1, 5, 9, 2, 6];
            assert_eq!(list_max_idiomatic(&xs), Some(9));
            assert_eq!(list_max_recursive(&xs), Some(9));
            assert_eq!(list_max_fold(&xs), Some(9));
        }
    
        #[test]
        fn test_max_empty() {
            let xs: &[i32] = &[];
            assert_eq!(list_max_idiomatic(xs), None);
            assert_eq!(list_max_recursive(xs), None);
            assert_eq!(list_max_fold(xs), None);
        }
    
        #[test]
        fn test_max_single() {
            assert_eq!(list_max_idiomatic(&[42]), Some(42));
            assert_eq!(list_max_recursive(&[42]), Some(42));
            assert_eq!(list_max_fold(&[42]), Some(42));
        }
    
        #[test]
        fn test_max_negative() {
            let xs = [-5, -1, -10, -3];
            assert_eq!(list_max_idiomatic(&xs), Some(-1));
            assert_eq!(list_max_recursive(&xs), Some(-1));
            assert_eq!(list_max_fold(&xs), Some(-1));
        }
    
        #[test]
        fn test_safe_head() {
            assert_eq!(safe_head_idiomatic(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head_recursive(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head_idiomatic(&[]), None);
            assert_eq!(safe_head_recursive(&[]), None);
        }
    
        #[test]
        fn test_double_max() {
            let xs = [3, 1, 4, 1, 5, 9, 2, 6];
            assert_eq!(double_max_idiomatic(&xs), Some(18));
        }
    
        #[test]
        fn test_double_max_empty() {
            assert_eq!(double_max_idiomatic(&[]), None);
        }
    
        #[test]
        fn test_option_map_manual() {
            assert_eq!(option_map(|x: i32| x * 2, Some(5)), Some(10));
            assert_eq!(option_map(|x: i32| x * 2, None), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_max_basic() {
            let xs = [3, 1, 4, 1, 5, 9, 2, 6];
            assert_eq!(list_max_idiomatic(&xs), Some(9));
            assert_eq!(list_max_recursive(&xs), Some(9));
            assert_eq!(list_max_fold(&xs), Some(9));
        }
    
        #[test]
        fn test_max_empty() {
            let xs: &[i32] = &[];
            assert_eq!(list_max_idiomatic(xs), None);
            assert_eq!(list_max_recursive(xs), None);
            assert_eq!(list_max_fold(xs), None);
        }
    
        #[test]
        fn test_max_single() {
            assert_eq!(list_max_idiomatic(&[42]), Some(42));
            assert_eq!(list_max_recursive(&[42]), Some(42));
            assert_eq!(list_max_fold(&[42]), Some(42));
        }
    
        #[test]
        fn test_max_negative() {
            let xs = [-5, -1, -10, -3];
            assert_eq!(list_max_idiomatic(&xs), Some(-1));
            assert_eq!(list_max_recursive(&xs), Some(-1));
            assert_eq!(list_max_fold(&xs), Some(-1));
        }
    
        #[test]
        fn test_safe_head() {
            assert_eq!(safe_head_idiomatic(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head_recursive(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head_idiomatic(&[]), None);
            assert_eq!(safe_head_recursive(&[]), None);
        }
    
        #[test]
        fn test_double_max() {
            let xs = [3, 1, 4, 1, 5, 9, 2, 6];
            assert_eq!(double_max_idiomatic(&xs), Some(18));
        }
    
        #[test]
        fn test_double_max_empty() {
            assert_eq!(double_max_idiomatic(&[]), None);
        }
    
        #[test]
        fn test_option_map_manual() {
            assert_eq!(option_map(|x: i32| x * 2, Some(5)), Some(10));
            assert_eq!(option_map(|x: i32| x * 2, None), None);
        }
    }

    Deep Comparison

    Comparison: Option Type — Safe List Maximum — OCaml vs Rust

    OCaml

    let rec list_max = function
      | []     -> None
      | h :: t ->
        begin match list_max t with
        | None   -> Some h
        | Some m -> Some (max h m)
        end
    
    let safe_head = function
      | []     -> None
      | h :: _ -> Some h
    
    let option_map f = function
      | None   -> None
      | Some x -> Some (f x)
    

    Rust — Idiomatic

    pub fn list_max_idiomatic(xs: &[i32]) -> Option<i32> {
        xs.iter().copied().max()
    }
    
    pub fn safe_head_idiomatic(xs: &[i32]) -> Option<i32> {
        xs.first().copied()
    }
    
    pub fn double_max_idiomatic(xs: &[i32]) -> Option<i32> {
        list_max_idiomatic(xs).map(|x| x * 2)
    }
    

    Rust — Functional (Recursive)

    pub fn list_max_recursive(xs: &[i32]) -> Option<i32> {
        match xs {
            [] => None,
            [head, tail @ ..] => match list_max_recursive(tail) {
                None => Some(*head),
                Some(m) => Some(if *head > m { *head } else { m }),
            },
        }
    }
    

    Rust — Fold-based

    pub fn list_max_fold(xs: &[i32]) -> Option<i32> {
        let (&first, rest) = xs.split_first()?;
        Some(rest.iter().fold(first, |acc, &x| acc.max(x)))
    }
    

    Comparison Table

    AspectOCamlRust
    Option type'a option = None \| Some of 'aOption<T> = None \| Some(T)
    Pattern matchingmatch ... with None -> ... \| Some x -> ...match ... { None => ..., Some(x) => ... }
    Map over optionCustom option_map or Option.mapBuilt-in Option::map
    ChainingOption.bind.and_then() or ? operator
    Max of iteratorManual recursioniter().max() returns Option
    Head of listManual pattern matchslice.first() returns Option<&T>
    Copy from refNot needed (no ref distinction).copied() converts Option<&T>Option<T>

    Type Signatures Explained

    OCaml: val list_max : 'a list -> 'a option — polymorphic over any ordered type (uses structural comparison)

    Rust: fn list_max_idiomatic(xs: &[i32]) -> Option<i32> — concrete type. Generic version would be fn list_max<T: Ord + Copy>(xs: &[T]) -> Option<T> requiring explicit trait bounds.

    Takeaways

  • Option is the same idea in both languages — Some/None with exhaustive matching, no nulls
  • Rust's Option is richer — 40+ methods (map, and_then, unwrap_or, filter, zip, ?)
  • **The ? operator** is Rust's killer feature for Option — xs.split_first()? propagates None elegantly
  • **.copied() bridge** is needed because Rust distinguishes &T from T; OCaml doesn't
  • Iterator integration means Rust rarely needs custom list_maxiter().max() is built-in and returns Option
  • Exercises

  • Implement safe_min alongside safe_max, then write safe_range that returns Option<(T, T)> with the min and max in a single pass.
  • Write safe_max_by_key that accepts a key extraction function f: &T -> K and returns the element with the greatest key, returning None for an empty list.
  • Implement top_n that returns the n largest elements from a slice as a sorted Vec<T>, using Option throughout to handle edge cases (empty slice, n > len).
  • Top-k: Implement top_k<T: Ord>(iter: impl Iterator<Item = T>, k: usize) -> Vec<T> that returns the k largest elements, using a min-heap of size k (BinaryHeap in Rust can be adapted).
  • Safe statistics: Implement safe_stats(data: &[f64]) -> Option<(f64, f64, f64)> returning (min, max, mean) — return None for empty input, using Iterator::min_by, max_by, and sum / count.
  • Open Source Repos