ExamplesBy LevelBy TopicLearning Paths
927 Intermediate

927-option-result — Option and Result

Functional Programming

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

  • • Use Option<T> for nullable lookups and safe head/tail operations
  • • Chain Option values with .map() and .and_then() (monadic bind)
  • • Use Result<T, E> for fallible operations with typed errors
  • • Chain Result values with .map() and .and_then()
  • • Use the ? operator for ergonomic error propagation
  • Code 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

  • ? operator: Rust's ? short-circuits on None/Err with one character; OCaml requires let* or explicit Result.bind calls.
  • Error conversion: Rust ? automatically converts error types using From; OCaml requires explicit Result.map_error.
  • Standard combinators: Both provide map, bind/and_then, unwrap_or, get_or_else; names differ slightly.
  • Exhaustiveness: Both enforce handling both variants in pattern matching; unwrapping without checking is unsafe in both (Rust 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);
        }
    }
    ✓ Tests Rust test suite
    #[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 exceptionsResult is the only way. The ? operator makes error propagation concise.

    Key Differences

    AspectOCamlRust
    Optional values'a optionOption<T>
    Error handling('a, 'b) result + exceptionsResult<T, E> only
    Chaining\|> pipe + Option.map/bind.map() / .and_then() / ?
    Error propagationManual matching or Result.bind? operator (sugar for match)
    UnwrappingOption.get (raises).unwrap() (panics)
    Default valuesOption.value ~default:x.unwrap_or(x)
    ConversionOption.to_result.ok_or(err) / .ok()

    What Rust Learners Should Notice

  • • **The ? operator is magical**: It replaces what would be verbose match expressions. let x = expr?; unwraps Ok/Some or returns early with Err/None.
  • No exceptions in Rust: OCaml lets you raise and try/with. Rust forces Result everywhere — more verbose but no hidden control flow.
  • Combinators are the same idea: OCaml's 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

  • • [The Rust Book — Error Handling](https://doc.rust-lang.org/book/ch09-00-error-handling.html)
  • • [OCaml Manual — Option](https://v2.ocaml.org/api/Option.html)
  • • [Rust by Example — Result](https://doc.rust-lang.org/rust-by-example/error/result.html)
  • Exercises

  • Implement 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.
  • Write accumulate_errors(validations: Vec<Result<(), String>>) -> Result<(), Vec<String>> that collects all errors instead of stopping at the first.
  • Implement a small expression evaluator using Result<f64, String> for division-by-zero and parse errors, using ? throughout.
  • Open Source Repos