๐Ÿฆ€ Functional Rust
๐ŸŽฌ Error Handling in Rust Option, Result, the ? operator, and combinators.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Option represents a value that may or may not exist โ€” Some(value) or None

โ€ข Result represents success (Ok) or failure (Err) โ€” no exceptions needed

โ€ข The ? operator propagates errors up the call stack concisely

โ€ข Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations

โ€ข The compiler forces you to handle every error case โ€” no silent failures

004: Option and Result

Difficulty: โญโญ Level: Intermediate Replace `null` crashes and uncaught exceptions with types the compiler forces you to handle.

The Problem This Solves

Every programmer has seen `NullPointerException`, `TypeError: Cannot read property of undefined`, or `AttributeError: 'NoneType' object`. These happen when code assumes a value exists, but it doesn't. The fix is always the same: check first. But it's easy to forget, the check is often far from the code that fails, and tests don't always catch it. Rust has no `null`. Instead, it has two types: The key difference from other languages: you can't use an `Option<T>` as if it were a `T`. The compiler refuses. You must handle the `None` case explicitly before you can access the value inside.

The Intuition

# Python โ€” None is a valid value anywhere, crashes at runtime
def safe_div(a, b):
 if b == 0:
     return None     # caller might forget to check this!
 return a / b

result = safe_div(10, 0)
print(result + 1)    # AttributeError: NoneType โ€” runtime crash
// Rust โ€” Option forces you to check at compile time
fn safe_div(a: f64, b: f64) -> Option<f64> {
 if b == 0.0 { None } else { Some(a / b) }
}

let result = safe_div(10.0, 0.0);
println!("{}", result + 1.0);   // COMPILE ERROR: can't use Option<f64> as f64
The Rust version catches the bug before the program ever runs.

How It Works in Rust

Option โ€” for values that might not exist:
// Safe division
fn safe_div(a: f64, b: f64) -> Option<f64> {
 if b == 0.0 { None } else { Some(a / b) }
}

// Using Option:
match safe_div(10.0, 2.0) {
 Some(result) => println!("{}", result),   // 5.0
 None         => println!("Division by zero"),
}

// Chaining with .map() โ€” transform the value if it exists
let doubled = safe_div(10.0, 2.0).map(|x| x * 2.0);  // Some(10.0)
let nothing = safe_div(10.0, 0.0).map(|x| x * 2.0);  // None โ€” map skips

// Default value
let result = safe_div(10.0, 0.0).unwrap_or(0.0);  // 0.0
Result โ€” for operations that fail with a reason:
#[derive(Debug)]
enum MathError {
 DivisionByZero,
 NegativeSquareRoot,
}

fn checked_div(a: i64, b: i64) -> Result<i64, MathError> {
 if b == 0 { Err(MathError::DivisionByZero) } else { Ok(a / b) }
}

fn checked_sqrt(x: f64) -> Result<f64, MathError> {
 if x < 0.0 { Err(MathError::NegativeSquareRoot) } else { Ok(x.sqrt()) }
}
The `?` operator โ€” chain fallible operations cleanly:
// Compute sqrt(a / b) โ€” two things that can fail
fn sqrt_of_division(a: f64, b: f64) -> Result<f64, MathError> {
 let quotient = safe_div(a, b).ok_or(MathError::DivisionByZero)?;
 //                                                               ^
 //             If this is Err, return immediately with that error
 checked_sqrt(quotient)
 // If this is Err, return that error too
}
`?` is Rust's "propagate error upward" operator. It replaces chains of `if err != nil { return err }` in Go, or try/catch in Java. Errors flow up automatically, and the code reads like the happy path.

What This Unlocks

Key Differences

ConceptOCamlRust
"Maybe a value"`'a option``Option<T>`
Success/failure`('a, 'b) result``Result<T, E>`
Chaining Options`Option.map`, `Option.bind``.map()`, `.and_then()`
Chaining Results`Result.bind`, `\>``.map()`, `.and_then()`, `?`
Propagate errorManual match or `Result.bind``?` operator
Force unwrap`Option.get` (raises)`.unwrap()` (panics)
/// 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);
    }
}

fn main() {
    println!("{:?}", safe_div(10.0, 2.0), Some(5.0));
    println!("{:?}", safe_div(1.0, 0.0), None);
    println!("{:?}", head(&[1, 2, 3]), Some(&1));
}
(* Option and Result: safe error handling without exceptions *)

(* โ”€โ”€ Option: safe lookups โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

let safe_div a b =
  if b = 0.0 then None else Some (a /. b)

let head = function
  | [] -> None
  | h :: _ -> Some h

let last lst =
  let rec aux = function
    | [] -> None
    | [x] -> Some x
    | _ :: t -> aux t
  in aux lst

(* โ”€โ”€ Option combinators โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

let find_and_double lst pred =
  List.find_opt pred lst |> Option.map (fun x -> x * 2)

let nth_or_default lst n default =
  match List.nth_opt lst n with
  | Some v -> v
  | None -> default

(* โ”€โ”€ Result type (OCaml 4.03+) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

type math_error = Division_by_zero_err | Negative_sqrt | Overflow_err

let checked_div a b =
  if b = 0 then Error Division_by_zero_err
  else Ok (a / b)

let checked_sqrt x =
  if x < 0.0 then Error Negative_sqrt
  else Ok (sqrt x)

(* โ”€โ”€ Monadic chaining with Result.bind โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

let sqrt_of_division a b =
  safe_div a b
  |> Option.to_result ~none:Division_by_zero_err
  |> Result.bind (fun q -> checked_sqrt q)

(* โ”€โ”€ Recursive association list lookup โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)

let rec assoc_opt key = function
  | [] -> None
  | (k, v) :: _ when k = key -> Some v
  | _ :: rest -> assoc_opt key rest

(* โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ *)
let () =
  assert (safe_div 10.0 2.0 = Some 5.0);
  assert (safe_div 1.0 0.0 = None);
  assert (head [1;2;3] = Some 1);
  assert (head [] = None);
  assert (last [1;2;3] = Some 3);
  assert (find_and_double [1;2;3;4] (fun x -> x > 2) = Some 6);
  assert (nth_or_default [10;20;30] 1 0 = 20);
  assert (nth_or_default [10;20;30] 5 99 = 99);
  assert (checked_div 10 2 = Ok 5);
  assert (checked_div 10 0 = Error Division_by_zero_err);
  assert (assoc_opt 2 [(1,"one");(2,"two")] = Some "two");
  assert (assoc_opt 9 [(1,"one")] = None);
  print_endline "โœ“ All option/result tests passed"

๐Ÿ“Š Detailed 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:
๐Ÿช Show OCaml equivalent
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

AspectOCamlRust
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 propagationManual 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

  • 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)