927-option-result — Option and Result
Tutorial
The Problem
Null pointer dereferences are the billion-dollar mistake — Tony Hoare's famous regret. Languages like ML, Haskell, Rust, and Swift replace null with explicit optional types: option / Option<T>. The type system forces the programmer to handle the "not present" case. For errors, exceptions are non-local, untyped, and easy to forget. Result<T, E> (or either in Haskell) makes errors explicit in the type signature. OCaml has option and result; Rust has Option<T> and Result<T, E>. Both provide map, and_then, unwrap_or combinators for composing safe computations.
🎯 Learning Outcomes
Option<T> for nullable lookups and safe head/tail operations.map() and .and_then() (monadic bind)Result<T, E> for fallible operations with typed errors.map() and .and_then()? operator for ergonomic error propagationCode Example
fn safe_div(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
// The ? operator propagates errors — Rust's monadic bind
fn sqrt_of_div(a: f64, b: f64) -> Result<f64, MathError> {
let q = safe_div(a, b).ok_or(MathError::DivisionByZero)?;
checked_sqrt(q)
}Key Differences
? short-circuits on None/Err with one character; OCaml requires let* or explicit Result.bind calls.? automatically converts error types using From; OCaml requires explicit Result.map_error.map, bind/and_then, unwrap_or, get_or_else; names differ slightly.unwrap() panics, OCaml Option.get raises).OCaml Approach
OCaml's option and result types have the same structure. Option.bind: 'a option -> ('a -> 'b option) -> 'b option is and_then. Option.map: ('a -> 'b) -> 'a option -> 'b option. Result.bind and Result.map are analogous. OCaml 4.08+ adds let* operator for monadic bind in Option and Result: let* n = parse_int s in let* n = positive n in sqrt_safe n. OCaml lacks the ? operator but let* provides equivalent ergonomics when used with Option.syntax or Result.syntax.
Full Source
#![allow(clippy::all)]
/// Option and Result: safe error handling without exceptions.
///
/// OCaml uses `option` and `result` types. Rust has `Option<T>` and `Result<T, E>`.
/// Both replace null/exceptions with types the compiler forces you to handle.
// ── Option: safe lookups ────────────────────────────────────────────────────
/// Find first element matching predicate (idiomatic Rust)
pub fn find_first<T>(list: &[T], pred: impl Fn(&T) -> bool) -> Option<&T> {
list.iter().find(|x| pred(x))
}
/// Safe division — returns None on divide-by-zero
pub fn safe_div(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
/// Safe head of list
pub fn head<T>(list: &[T]) -> Option<&T> {
list.first()
}
/// Safe last element
pub fn last<T>(list: &[T]) -> Option<&T> {
list.last()
}
// ── Option combinators (functional chaining) ────────────────────────────────
/// Chain optional operations: find element, then transform it
pub fn find_and_double(list: &[i64], pred: impl Fn(&i64) -> bool) -> Option<i64> {
list.iter().find(|x| pred(x)).map(|x| x * 2)
}
/// Get the nth element safely, with a default
pub fn nth_or_default<T: Clone>(list: &[T], n: usize, default: T) -> T {
list.get(n).cloned().unwrap_or(default)
}
// ── Result: error handling with context ─────────────────────────────────────
#[derive(Debug, PartialEq)]
pub enum MathError {
DivisionByZero,
NegativeSquareRoot,
Overflow,
}
/// Safe division returning Result with error context
pub fn checked_div(a: i64, b: i64) -> Result<i64, MathError> {
if b == 0 {
Err(MathError::DivisionByZero)
} else {
a.checked_div(b).ok_or(MathError::Overflow)
}
}
/// Safe square root
pub fn checked_sqrt(x: f64) -> Result<f64, MathError> {
if x < 0.0 {
Err(MathError::NegativeSquareRoot)
} else {
Ok(x.sqrt())
}
}
/// Chain computations with `?` operator — Rust's monadic bind
/// Computes: sqrt(a / b)
pub fn sqrt_of_division(a: f64, b: f64) -> Result<f64, MathError> {
let quotient = safe_div(a, b).ok_or(MathError::DivisionByZero)?;
checked_sqrt(quotient)
}
// ── Recursive style: Option threading ───────────────────────────────────────
/// Recursive lookup in an association list (like OCaml's List.assoc_opt)
pub fn assoc_opt<'a, K: PartialEq, V>(key: &K, pairs: &'a [(K, V)]) -> Option<&'a V> {
match pairs.split_first() {
None => None,
Some(((k, v), _)) if k == key => Some(v),
Some((_, rest)) => assoc_opt(key, rest),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10.0, 2.0), Some(5.0));
assert_eq!(safe_div(1.0, 0.0), None);
}
#[test]
fn test_head_last() {
assert_eq!(head(&[1, 2, 3]), Some(&1));
assert_eq!(last(&[1, 2, 3]), Some(&3));
assert_eq!(head::<i32>(&[]), None);
assert_eq!(last::<i32>(&[]), None);
}
#[test]
fn test_find_and_double() {
assert_eq!(find_and_double(&[1, 2, 3, 4], |x| *x > 2), Some(6));
assert_eq!(find_and_double(&[1, 2], |x| *x > 10), None);
}
#[test]
fn test_nth_or_default() {
assert_eq!(nth_or_default(&[10, 20, 30], 1, 0), 20);
assert_eq!(nth_or_default(&[10, 20, 30], 5, 99), 99);
assert_eq!(nth_or_default::<i32>(&[], 0, -1), -1);
}
#[test]
fn test_checked_div() {
assert_eq!(checked_div(10, 2), Ok(5));
assert_eq!(checked_div(10, 0), Err(MathError::DivisionByZero));
}
#[test]
fn test_checked_sqrt() {
assert!((checked_sqrt(4.0).unwrap() - 2.0).abs() < 1e-10);
assert_eq!(checked_sqrt(-1.0), Err(MathError::NegativeSquareRoot));
}
#[test]
fn test_sqrt_of_division_chaining() {
let r = sqrt_of_division(16.0, 4.0).unwrap();
assert!((r - 2.0).abs() < 1e-10);
assert_eq!(sqrt_of_division(16.0, 0.0), Err(MathError::DivisionByZero));
assert_eq!(
sqrt_of_division(-16.0, 1.0),
Err(MathError::NegativeSquareRoot)
);
}
#[test]
fn test_assoc_opt() {
let pairs = vec![(1, "one"), (2, "two"), (3, "three")];
assert_eq!(assoc_opt(&2, &pairs), Some(&"two"));
assert_eq!(assoc_opt(&99, &pairs), None);
assert_eq!(assoc_opt::<i32, &str>(&1, &[]), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10.0, 2.0), Some(5.0));
assert_eq!(safe_div(1.0, 0.0), None);
}
#[test]
fn test_head_last() {
assert_eq!(head(&[1, 2, 3]), Some(&1));
assert_eq!(last(&[1, 2, 3]), Some(&3));
assert_eq!(head::<i32>(&[]), None);
assert_eq!(last::<i32>(&[]), None);
}
#[test]
fn test_find_and_double() {
assert_eq!(find_and_double(&[1, 2, 3, 4], |x| *x > 2), Some(6));
assert_eq!(find_and_double(&[1, 2], |x| *x > 10), None);
}
#[test]
fn test_nth_or_default() {
assert_eq!(nth_or_default(&[10, 20, 30], 1, 0), 20);
assert_eq!(nth_or_default(&[10, 20, 30], 5, 99), 99);
assert_eq!(nth_or_default::<i32>(&[], 0, -1), -1);
}
#[test]
fn test_checked_div() {
assert_eq!(checked_div(10, 2), Ok(5));
assert_eq!(checked_div(10, 0), Err(MathError::DivisionByZero));
}
#[test]
fn test_checked_sqrt() {
assert!((checked_sqrt(4.0).unwrap() - 2.0).abs() < 1e-10);
assert_eq!(checked_sqrt(-1.0), Err(MathError::NegativeSquareRoot));
}
#[test]
fn test_sqrt_of_division_chaining() {
let r = sqrt_of_division(16.0, 4.0).unwrap();
assert!((r - 2.0).abs() < 1e-10);
assert_eq!(sqrt_of_division(16.0, 0.0), Err(MathError::DivisionByZero));
assert_eq!(
sqrt_of_division(-16.0, 1.0),
Err(MathError::NegativeSquareRoot)
);
}
#[test]
fn test_assoc_opt() {
let pairs = vec![(1, "one"), (2, "two"), (3, "three")];
assert_eq!(assoc_opt(&2, &pairs), Some(&"two"));
assert_eq!(assoc_opt(&99, &pairs), None);
assert_eq!(assoc_opt::<i32, &str>(&1, &[]), None);
}
}
Deep Comparison
Option and Result: OCaml vs Rust
The Core Insight
Both languages solve the "billion dollar mistake" (null references) with sum types: Option for optional values and Result for computations that may fail. The compiler forces you to handle both cases — no more NullPointerException or unhandled exceptions. This is perhaps the strongest argument for ML-family type systems.
OCaml Approach
OCaml's option type (None | Some 'a) and result type (Ok 'a | Error 'b) work with pattern matching and the pipe operator:
let safe_div a b =
if b = 0.0 then None else Some (a /. b)
(* Chain with |> and Option.map *)
safe_div 10.0 2.0 |> Option.map (fun x -> x *. x)
OCaml also has exceptions (raise, try...with), giving you a choice. Idiomatic OCaml increasingly favors result for recoverable errors.
Rust Approach
Rust has Option<T> and Result<T, E> as core types with rich combinator methods:
fn safe_div(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
// The ? operator propagates errors — Rust's monadic bind
fn sqrt_of_div(a: f64, b: f64) -> Result<f64, MathError> {
let q = safe_div(a, b).ok_or(MathError::DivisionByZero)?;
checked_sqrt(q)
}
Rust has no exceptions — Result is the only way. The ? operator makes error propagation concise.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Optional values | 'a option | Option<T> |
| Error handling | ('a, 'b) result + exceptions | Result<T, E> only |
| Chaining | \|> pipe + Option.map/bind | .map() / .and_then() / ? |
| Error propagation | Manual matching or Result.bind | ? operator (sugar for match) |
| Unwrapping | Option.get (raises) | .unwrap() (panics) |
| Default values | Option.value ~default:x | .unwrap_or(x) |
| Conversion | Option.to_result | .ok_or(err) / .ok() |
What Rust Learners Should Notice
? operator is magical**: It replaces what would be verbose match expressions. let x = expr?; unwraps Ok/Some or returns early with Err/None.raise and try/with. Rust forces Result everywhere — more verbose but no hidden control flow.Option.map f x is Rust's x.map(f). Method syntax vs function syntax, same concept.unwrap() is a code smell**: Just like OCaml's Option.get can raise, Rust's .unwrap() panics. Both should be avoided in production code.Further Reading
Exercises
safe_chain<T, U, V>(opt: Option<T>, f: impl Fn(T) -> Option<U>, g: impl Fn(U) -> Option<V>) -> Option<V> using and_then.accumulate_errors(validations: Vec<Result<(), String>>) -> Result<(), Vec<String>> that collects all errors instead of stopping at the first.Result<f64, String> for division-by-zero and parse errors, using ? throughout.