๐Ÿฆ€ 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

055: Option Monad

Difficulty: โญโญ Level: Intermediate Chain fallible operations on `Option` without nesting โ€” using `and_then`, the monadic bind you already know.

The Problem This Solves

You're writing code where each step might produce nothing. Maybe you're looking up a key in a map, then using that value to look up another key, then checking a condition on the result. Each step returns `Option<T>`. The natural instinct is to nest:
// Without and_then: the pyramid of doom
fn find_user_docs(env: &HashMap<&str, &str>, paths: &HashMap<&str, Vec<&str>>) -> Option<String> {
 match env.get("HOME") {
     None => None,
     Some(home) => match paths.get(home.to_owned()) {
         None => None,
         Some(dirs) => {
             if dirs.contains(&"documents") {
                 Some("documents found".to_string())
             } else {
                 None
             }
         }
     }
 }
}
Three levels of nesting for three fallible steps. Add two more steps and you're off the right edge of your screen. The logic is buried inside the matching. It's hard to read, hard to refactor, and easy to miss a case. The real pain: `None` propagates identically through every step โ€” if any step fails, the whole chain fails. You're writing the same boilerplate over and over to express a simple idea: "do this, then this, then this, and if anything is missing, bail out." The Option monad exists to solve exactly that pain.

The Intuition

Think of a production line. Each station takes a part, does something with it, and passes it to the next station โ€” or sends a "rejected" signal that skips all remaining stations automatically. `Option` is that production line. `None` is the rejection signal. Once you get `None`, every subsequent step is skipped and `None` comes out the end. You don't have to check at every station; the line handles it. `and_then` is the conveyor belt connecting stations. It says: "if I have a value, pass it to the next function; if I have `None`, skip everything and propagate `None`."
// The idea in code:
Some(value)
 .and_then(|v| do_something(v))   // runs if Some
 .and_then(|v| do_more(v))        // runs if still Some
 .and_then(|v| final_step(v))     // runs if still Some
// Result: Some(final) or None (wherever the line broke)
This is a monad: a pattern for chaining operations that might fail (or have effects), without nesting. The word sounds academic but the behavior is familiar. You already use it every time you write `?` in a function that returns `Option`. `?` is literally `and_then` with early return. They're the same thing written differently.

How It Works in Rust

Start simple: one fallible step
fn safe_div(x: i32, y: i32) -> Option<i32> {
 if y == 0 { None } else { Some(x / y) }
}

let result = safe_div(10, 2); // Some(5)
let result = safe_div(10, 0); // None
Chain two steps with `and_then`
fn safe_sqrt(x: i32) -> Option<f64> {
 if x < 0 { None } else { Some((x as f64).sqrt()) }
}

// and_then: "if I have a value, run this function on it"
// The function must return Option โ€” it decides whether to continue or bail
let result = safe_div(100, 4)          // Some(25)
 .and_then(|q| safe_sqrt(q))        // Some(5.0)
 .map(|r| r as i32);                // Some(5)
                                    // (map is for infallible transforms)

let result = safe_div(100, 0)          // None (divide by zero)
 .and_then(|q| safe_sqrt(q))        // skipped โ€” None propagates
 .map(|r| r as i32);                // skipped โ€” still None
The same logic with `?` operator
fn compute(a: i32, b: i32) -> Option<i32> {
 let q = safe_div(a, b)?;   // ? means: unwrap or return None immediately
 let r = safe_sqrt(q)?;     // same
 Some(r as i32)
}
`?` and `.and_then()` chains are identical in what they do. `?` is just easier to read when you have many steps. Both are the Option monad in action. A real-world lookup chain
fn find_user_docs(env: &HashMap<&str, &str>, paths: &HashMap<&str, Vec<&str>>) -> Option<String> {
 env.get("HOME")                          // Option<&&str>
     .and_then(|home| paths.get(home.to_owned())) // Option<&Vec<&str>>
     .and_then(|dirs| {
         if dirs.contains(&"documents") {
             Some("documents found".to_string())
         } else {
             None
         }
     })
}
// If HOME is missing โ†’ None. If path not found โ†’ None. If no documents โ†’ None.
// No nested matches. Three steps, three lines.

What This Unlocks

Key Differences

ConceptOCamlRust
Monadic bind`Option.bind` / `>>=` operator`Option::and_then`
Do-notation sugarNot built in (use `let*` in newer OCaml)`?` operator
`None` propagationAutomatic in bind chainAutomatic in `and_then` / `?`
Value ownershipShared (immutable GC'd values)`and_then` consumes the `Option`
Naming`bind`, `return` (theory names)`and_then`, `Some` (practical names)
// Example 055: Option Monad
// Monadic bind (and_then) for Option: chain computations that may fail

use std::collections::HashMap;

// Approach 1: Safe lookup chain using and_then
fn find_user_docs(env: &HashMap<&str, &str>, paths: &HashMap<&str, Vec<&str>>) -> Option<String> {
    env.get("HOME")
        .and_then(|home| paths.get(home.to_owned()))
        .and_then(|dirs| {
            if dirs.contains(&"documents") {
                Some("documents found".to_string())
            } else {
                None
            }
        })
}

// Approach 2: Safe arithmetic chain
fn safe_div(x: i32, y: i32) -> Option<i32> {
    if y == 0 { None } else { Some(x / y) }
}

fn safe_sqrt(x: i32) -> Option<f64> {
    if x < 0 { None } else { Some((x as f64).sqrt()) }
}

fn compute(a: i32, b: i32) -> Option<i32> {
    safe_div(a, b)
        .and_then(|q| safe_sqrt(q))
        .map(|r| r as i32)
}

// Approach 3: Using ? operator (Rust's monadic sugar for Option)
fn compute_question_mark(a: i32, b: i32) -> Option<i32> {
    let q = safe_div(a, b)?;
    let r = safe_sqrt(q)?;
    Some(r as i32)
}

fn main() {
    let mut env = HashMap::new();
    env.insert("HOME", "/home/user");
    env.insert("USER", "alice");

    let mut paths = HashMap::new();
    paths.insert("/home/user", vec!["documents", "photos"]);

    println!("Docs: {:?}", find_user_docs(&env, &paths));
    println!("compute(100, 4) = {:?}", compute(100, 4));
    println!("compute(100, 0) = {:?}", compute(100, 0));
    println!("compute?(100, 4) = {:?}", compute_question_mark(100, 4));
}

#[cfg(test)]
mod tests {
    use super::*;

    fn setup() -> (HashMap<&'static str, &'static str>, HashMap<&'static str, Vec<&'static str>>) {
        let mut env = HashMap::new();
        env.insert("HOME", "/home/user");
        let mut paths = HashMap::new();
        paths.insert("/home/user", vec!["documents", "photos"]);
        (env, paths)
    }

    #[test]
    fn test_lookup_chain_success() {
        let (env, paths) = setup();
        assert_eq!(find_user_docs(&env, &paths), Some("documents found".to_string()));
    }

    #[test]
    fn test_lookup_chain_missing_key() {
        let env = HashMap::new();
        let paths = HashMap::new();
        assert_eq!(find_user_docs(&env, &paths), None);
    }

    #[test]
    fn test_safe_div_success() {
        assert_eq!(safe_div(10, 2), Some(5));
    }

    #[test]
    fn test_safe_div_by_zero() {
        assert_eq!(safe_div(10, 0), None);
    }

    #[test]
    fn test_compute_success() {
        assert_eq!(compute(100, 4), Some(5));
    }

    #[test]
    fn test_compute_div_zero() {
        assert_eq!(compute(100, 0), None);
    }

    #[test]
    fn test_compute_negative_sqrt() {
        assert_eq!(compute(-100, 1), None);
    }

    #[test]
    fn test_question_mark_same_as_and_then() {
        assert_eq!(compute(100, 4), compute_question_mark(100, 4));
        assert_eq!(compute(100, 0), compute_question_mark(100, 0));
        assert_eq!(compute(-100, 1), compute_question_mark(-100, 1));
    }
}
(* Example 055: Option Monad *)
(* Monadic bind (>>=) for Option: chain computations that may fail *)

let return_ x = Some x
let bind m f = match m with None -> None | Some x -> f x
let ( >>= ) = bind

(* Approach 1: Safe dictionary lookup chain *)
let lookup key assoc = List.assoc_opt key assoc

let env = [("HOME", "/home/user"); ("USER", "alice")]
let paths = [("/home/user", ["documents"; "photos"])]

let find_user_docs () =
  lookup "HOME" env >>= fun home ->
  lookup home paths >>= fun dirs ->
  if List.mem "documents" dirs then Some "documents found"
  else None

(* Approach 2: Safe arithmetic chain *)
let safe_div x y = if y = 0 then None else Some (x / y)
let safe_sqrt x = if x < 0 then None else Some (Float.sqrt (Float.of_int x))

let compute a b =
  safe_div a b >>= fun q ->
  safe_sqrt q >>= fun r ->
  Some (Float.to_int r)

(* Approach 3: Using Option.bind from stdlib *)
let compute_stdlib a b =
  Option.bind (safe_div a b) (fun q ->
  Option.bind (safe_sqrt q) (fun r ->
  Some (Float.to_int r)))

let () =
  (* Lookup chain *)
  assert (find_user_docs () = Some "documents found");
  assert (lookup "MISSING" env >>= fun _ -> Some "x" = None);

  (* Arithmetic chain *)
  assert (compute 100 4 = Some 5);
  assert (compute 100 0 = None);  (* div by zero *)
  assert (compute (-100) 1 = None);  (* negative sqrt *)

  (* Stdlib version same results *)
  assert (compute_stdlib 100 4 = Some 5);
  assert (compute_stdlib 100 0 = None);

  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Option Monad

Monadic Bind

OCaml:

๐Ÿช Show OCaml equivalent
let bind m f = match m with None -> None | Some x -> f x
let ( >>= ) = bind

safe_div 100 4 >>= fun q ->
safe_sqrt q >>= fun r ->
Some (Float.to_int r)

Rust:

safe_div(100, 4)
 .and_then(|q| safe_sqrt(q))
 .map(|r| r as i32)

Rust's ? Operator (Monadic Sugar)

Rust:

fn compute(a: i32, b: i32) -> Option<i32> {
 let q = safe_div(a, b)?;   // returns None early if None
 let r = safe_sqrt(q)?;     // returns None early if None
 Some(r as i32)
}

Chained Lookups

OCaml:

๐Ÿช Show OCaml equivalent
lookup "HOME" env >>= fun home ->
lookup home paths >>= fun dirs ->
if List.mem "documents" dirs then Some "found" else None

Rust:

env.get("HOME")
 .and_then(|home| paths.get(*home))
 .and_then(|dirs| {
     if dirs.contains(&"documents") { Some("found") } else { None }
 })