ExamplesBy LevelBy TopicLearning Paths
004 Intermediate

004 — Option and Result

Functional Programming

Tutorial

The Problem

Null references were famously called "the billion-dollar mistake" by their inventor Tony Hoare. Languages like Java, C, and C++ use null/NULL to represent missing values, which causes NullPointerException, segfaults, and unchecked error codes at runtime. Functional languages solved this with algebraic types: Option (sometimes called Maybe) wraps a value that may or may not exist, and Result (or Either) wraps a value that may have failed with an error.

These types make the possibility of absence or failure explicit in the type signature, forcing callers to handle both cases. They also compose via map and and_then (monadic bind), enabling clean pipelines of fallible operations without nested if-let chains.

🎯 Learning Outcomes

  • • Use Option<T> for values that may not exist, avoiding null
  • • Use Result<T, E> for operations that may fail with a typed error
  • • Chain operations with .map() and .and_then() to avoid nested matching
  • • Understand the monadic structure: and_then is "do this next, but only if the previous step succeeded"
  • • Convert between Option and Result with .ok_or() and .ok()
  • • Use if let Some(x) = opt { ... } as shorthand when only the Some case needs handling, without writing a full match
  • Code Example

    #![allow(clippy::all)]
    // 004: Option and Result
    // Safe handling of missing values and errors
    
    // Approach 1: Option basics
    fn safe_div(a: i32, b: i32) -> Option<i32> {
        if b == 0 {
            None
        } else {
            Some(a / b)
        }
    }
    
    fn safe_head(v: &[i32]) -> Option<i32> {
        v.first().copied()
    }
    
    fn find_even(v: &[i32]) -> Option<i32> {
        v.iter().find(|&&x| x % 2 == 0).copied()
    }
    
    // Approach 2: Chaining with map and and_then
    fn double_head(v: &[i32]) -> Option<i32> {
        safe_head(v).map(|x| x * 2)
    }
    
    fn safe_div_then_add(a: i32, b: i32, c: i32) -> Option<i32> {
        safe_div(a, b).map(|q| q + c)
    }
    
    fn chain_lookups(v1: &[i32], v2: &[i32]) -> Option<i32> {
        safe_head(v1).and_then(|idx| v2.get(idx as usize).copied())
    }
    
    // Approach 3: Result for richer errors
    #[derive(Debug, PartialEq)]
    enum MyError {
        DivByZero,
        NegativeInput,
        EmptyList,
    }
    
    fn safe_div_r(a: i32, b: i32) -> Result<i32, MyError> {
        if b == 0 {
            Err(MyError::DivByZero)
        } else {
            Ok(a / b)
        }
    }
    
    fn safe_sqrt(x: f64) -> Result<f64, MyError> {
        if x < 0.0 {
            Err(MyError::NegativeInput)
        } else {
            Ok(x.sqrt())
        }
    }
    
    fn safe_head_r(v: &[i32]) -> Result<i32, MyError> {
        v.first().copied().ok_or(MyError::EmptyList)
    }
    
    fn compute(v: &[i32]) -> Result<i32, MyError> {
        let x = safe_head_r(v)?;
        safe_div_r(x * 10, 3)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_safe_div() {
            assert_eq!(safe_div(10, 3), Some(3));
            assert_eq!(safe_div(10, 0), None);
        }
    
        #[test]
        fn test_safe_head() {
            assert_eq!(safe_head(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head(&[]), None);
        }
    
        #[test]
        fn test_find_even() {
            assert_eq!(find_even(&[1, 3, 4, 5]), Some(4));
            assert_eq!(find_even(&[1, 3, 5]), None);
        }
    
        #[test]
        fn test_double_head() {
            assert_eq!(double_head(&[5, 10]), Some(10));
            assert_eq!(double_head(&[]), None);
        }
    
        #[test]
        fn test_chain_lookups() {
            assert_eq!(chain_lookups(&[1], &[10, 20, 30]), Some(20));
            assert_eq!(chain_lookups(&[], &[10, 20]), None);
        }
    
        #[test]
        fn test_result() {
            assert_eq!(safe_div_r(10, 2), Ok(5));
            assert_eq!(safe_div_r(10, 0), Err(MyError::DivByZero));
        }
    
        #[test]
        fn test_compute() {
            assert_eq!(compute(&[5, 10]), Ok(16));
            assert_eq!(compute(&[]), Err(MyError::EmptyList));
        }
    }

    Key Differences

  • **? operator**: Rust's ? in a function returning Result is syntactic sugar for and_then/early return. OCaml uses let* (ppx_let) or explicit match? does not exist in standard OCaml.
  • Error types: Rust's Result<T, E> is generic over the error type. OCaml's result type is also ('a, 'b) result but idiomatic OCaml often uses polymorphic variants for errors.
  • **ok_or**: Rust provides .ok_or(err) to convert Option<T> to Result<T, E>. OCaml uses Option.to_result ~none:err.
  • Unwrap: Both languages provide an "unwrap or panic" escape hatch (Rust: .unwrap(), OCaml: Option.get). Both should be avoided in production code.
  • Null safety: Both languages eliminate null at the type level. Option forces explicit handling of absence; there is no way to call a method on None by accident.
  • **? operator:** Rust's ? for early-return on error has no direct OCaml syntax equivalent (OCaml uses let* with ppx_let or explicit match). Both achieve the same monadic composition.
  • Error type: Rust's Result<T, E> carries a typed error E. OCaml's result type: type ('a, 'b) result = Ok of 'a | Error of 'b. Both are isomorphic.
  • Conversion: Rust provides .ok() (ResultOption), .ok_or(e) (OptionResult). OCaml uses manual match for these conversions.
  • OCaml Approach

    OCaml's option type (None | Some x) and result type (Ok x | Error e) work identically. Option.map and Option.bind correspond to Rust's .map() and .and_then(). The |> pipe makes chaining natural: safe_head lst |> Option.bind (fun idx -> ...). OCaml also has let* (monadic let) for sequential binding without nesting: let* x = safe_div a b in let* y = safe_sqrt x in Ok (x + y).

    Full Source

    #![allow(clippy::all)]
    // 004: Option and Result
    // Safe handling of missing values and errors
    
    // Approach 1: Option basics
    fn safe_div(a: i32, b: i32) -> Option<i32> {
        if b == 0 {
            None
        } else {
            Some(a / b)
        }
    }
    
    fn safe_head(v: &[i32]) -> Option<i32> {
        v.first().copied()
    }
    
    fn find_even(v: &[i32]) -> Option<i32> {
        v.iter().find(|&&x| x % 2 == 0).copied()
    }
    
    // Approach 2: Chaining with map and and_then
    fn double_head(v: &[i32]) -> Option<i32> {
        safe_head(v).map(|x| x * 2)
    }
    
    fn safe_div_then_add(a: i32, b: i32, c: i32) -> Option<i32> {
        safe_div(a, b).map(|q| q + c)
    }
    
    fn chain_lookups(v1: &[i32], v2: &[i32]) -> Option<i32> {
        safe_head(v1).and_then(|idx| v2.get(idx as usize).copied())
    }
    
    // Approach 3: Result for richer errors
    #[derive(Debug, PartialEq)]
    enum MyError {
        DivByZero,
        NegativeInput,
        EmptyList,
    }
    
    fn safe_div_r(a: i32, b: i32) -> Result<i32, MyError> {
        if b == 0 {
            Err(MyError::DivByZero)
        } else {
            Ok(a / b)
        }
    }
    
    fn safe_sqrt(x: f64) -> Result<f64, MyError> {
        if x < 0.0 {
            Err(MyError::NegativeInput)
        } else {
            Ok(x.sqrt())
        }
    }
    
    fn safe_head_r(v: &[i32]) -> Result<i32, MyError> {
        v.first().copied().ok_or(MyError::EmptyList)
    }
    
    fn compute(v: &[i32]) -> Result<i32, MyError> {
        let x = safe_head_r(v)?;
        safe_div_r(x * 10, 3)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_safe_div() {
            assert_eq!(safe_div(10, 3), Some(3));
            assert_eq!(safe_div(10, 0), None);
        }
    
        #[test]
        fn test_safe_head() {
            assert_eq!(safe_head(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head(&[]), None);
        }
    
        #[test]
        fn test_find_even() {
            assert_eq!(find_even(&[1, 3, 4, 5]), Some(4));
            assert_eq!(find_even(&[1, 3, 5]), None);
        }
    
        #[test]
        fn test_double_head() {
            assert_eq!(double_head(&[5, 10]), Some(10));
            assert_eq!(double_head(&[]), None);
        }
    
        #[test]
        fn test_chain_lookups() {
            assert_eq!(chain_lookups(&[1], &[10, 20, 30]), Some(20));
            assert_eq!(chain_lookups(&[], &[10, 20]), None);
        }
    
        #[test]
        fn test_result() {
            assert_eq!(safe_div_r(10, 2), Ok(5));
            assert_eq!(safe_div_r(10, 0), Err(MyError::DivByZero));
        }
    
        #[test]
        fn test_compute() {
            assert_eq!(compute(&[5, 10]), Ok(16));
            assert_eq!(compute(&[]), Err(MyError::EmptyList));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_safe_div() {
            assert_eq!(safe_div(10, 3), Some(3));
            assert_eq!(safe_div(10, 0), None);
        }
    
        #[test]
        fn test_safe_head() {
            assert_eq!(safe_head(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head(&[]), None);
        }
    
        #[test]
        fn test_find_even() {
            assert_eq!(find_even(&[1, 3, 4, 5]), Some(4));
            assert_eq!(find_even(&[1, 3, 5]), None);
        }
    
        #[test]
        fn test_double_head() {
            assert_eq!(double_head(&[5, 10]), Some(10));
            assert_eq!(double_head(&[]), None);
        }
    
        #[test]
        fn test_chain_lookups() {
            assert_eq!(chain_lookups(&[1], &[10, 20, 30]), Some(20));
            assert_eq!(chain_lookups(&[], &[10, 20]), None);
        }
    
        #[test]
        fn test_result() {
            assert_eq!(safe_div_r(10, 2), Ok(5));
            assert_eq!(safe_div_r(10, 0), Err(MyError::DivByZero));
        }
    
        #[test]
        fn test_compute() {
            assert_eq!(compute(&[5, 10]), Ok(16));
            assert_eq!(compute(&[]), Err(MyError::EmptyList));
        }
    }

    Deep Comparison

    Core Insight

    Option replaces null pointers. Result replaces exceptions. Both languages encode success/failure in the type system, forcing the caller to handle every case.

    OCaml Approach

  • option type: Some x | None
  • result type: Ok x | Error e
  • Option.map, Option.bind for chaining
  • Result.map, Result.bind for chaining
  • • Pattern matching is the primary way to unwrap
  • Rust Approach

  • Option<T>: Some(x) / None
  • Result<T, E>: Ok(x) / Err(e)
  • .map(), .and_then() for chaining
  • ? operator for early return on error
  • .unwrap_or(), .unwrap_or_else() for defaults
  • Comparison Table

    FeatureOCamlRust
    Option type'a optionOption<T>
    Result type('a, 'e) resultResult<T, E>
    MapOption.map f oo.map(f)
    Bind/FlatMapOption.bind o fo.and_then(f)
    DefaultOption.value ~default oo.unwrap_or(d)
    Error propagationPattern match? operator

    Exercises

  • Safe index: Write safe_get(v: &[i32], i: usize) -> Option<i32> and chain it with safe_div to implement divide_at_index(nums: &[i32], i: usize, divisor: i32) -> Option<i32>.
  • Collect options: Write all_or_none(opts: &[Option<i32>]) -> Option<Vec<i32>> that returns Some only if all inputs are Some, using .collect::<Option<Vec<_>>>().
  • Error enrichment: Write a function that parses a string to an integer, divides it by another parsed integer, and returns a Result<i32, String> with a descriptive error message at each step.
  • Optional chaining: Given a nested structure User { address: Option<Address> } where Address { city: Option<String> }, write a function that returns the city as Option<&str> using and_then and as_deref.
  • Collect Options: Implement sequence(opts: Vec<Option<T>>) -> Option<Vec<T>> that returns None if any element is None, otherwise returns Some of all the values — equivalent to OCaml's Option.join applied to a list.
  • Open Source Repos