Option Type — Safe List Maximum
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
option type to Rust's Option<T> (they're nearly identical)Some/None in both languagesmap instead of nested matching🦀 The Rust Way
iter().copied().max(), first().copied(), Option::map[head, tail @ ..] mirroring OCaml's h :: tsplit_first() + fold for iterative maximumCode 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
Option<T> in Rust ≈ 'a option in OCaml — same constructors, same philosophyOption has 40+ methods (map, and_then, unwrap_or, ? operator); OCaml's is simpler? operator:** Rust can propagate None with ? (like split_first()?); OCaml uses match or Option.bind.copied() to go from Option<&T> to Option<T>; OCaml doesn't distinguishmax() returns Option natively — no need for a custom functionmax() 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.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);
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Option type | 'a option = None \| Some of 'a | Option<T> = None \| Some(T) |
| Pattern matching | match ... with None -> ... \| Some x -> ... | match ... { None => ..., Some(x) => ... } |
| Map over option | Custom option_map or Option.map | Built-in Option::map |
| Chaining | Option.bind | .and_then() or ? operator |
| Max of iterator | Manual recursion | iter().max() returns Option |
| Head of list | Manual pattern match | slice.first() returns Option<&T> |
| Copy from ref | Not 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
Some/None with exhaustive matching, no nullsmap, and_then, unwrap_or, filter, zip, ?)? 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'tlist_max — iter().max() is built-in and returns OptionExercises
safe_min alongside safe_max, then write safe_range that returns Option<(T, T)> with the min and max in a single pass.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.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<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_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.