ExamplesBy LevelBy TopicLearning Paths
854 Expert

Option Monad

Functional Programming

Tutorial

The Problem

A chain of operations that might fail — look up a user, find their settings, get their preferred language — requires either nested if let Some blocks (deeply indented "pyramid of doom") or the ? operator shorthand. The Option monad formalizes this: and_then sequences computations where each step might return None, automatically propagating absence without explicit checking. This is Rust's Option::and_then, Haskell's Maybe monad, and OCaml's Option.bind. The power: code that looks like a straight pipeline reads cleanly, yet automatically handles every possible absence at every step.

🎯 Learning Outcomes

  • • Understand and_then (monadic bind): if Some(x), apply f to x and return f(x); if None, return None
  • • Chain multiple and_then calls to build pipelines of fallible lookups
  • • Recognize Rust's ? operator as syntactic sugar for and_then (or return Err())
  • • Understand the difference from map: map wraps the result; and_then expects f to return Option<U>
  • • Apply to: multi-step dictionary lookups, configuration path traversal, safe arithmetic chains
  • Code Example

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

    Key Differences

    AspectRustOCaml
    Bind functionOption::and_thenOption.bind
    Infix operator? in fn -> Option<T>>>= via let ( >>= )
    Do notation? operatorlet%bind with ppx_let
    Map vs bindmap for T -> U, and_then for T -> Option<U>Option.map vs Option.bind
    None propagationAutomatic via and_thenSame
    Chain lengthUnlimited and_then chainSame

    OCaml Approach

    OCaml's Option.bind is the and_then equivalent: Option.bind (Hashtbl.find_opt env "HOME") (fun home -> Hashtbl.find_opt paths home). The let ( >>= ) = Option.bind infix operator enables pipeline syntax: Hashtbl.find_opt env "HOME" >>= Hashtbl.find_opt paths >>= List.nth_opt. OCaml's ppx_let syntax extension allows let%bind home = ... for do-notation style. The Option.map at the end for the infallible transform mirrors the Rust pattern.

    Full Source

    #![allow(clippy::all)]
    // 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)
    }
    
    #[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));
        }
    }
    ✓ Tests Rust test suite
    #[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));
        }
    }

    Deep Comparison

    Comparison: Option Monad

    Monadic Bind

    OCaml:

    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:

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

    Exercises

  • Rewrite find_user_docs using the ? operator inside a function returning Option<String> and verify same behavior.
  • Implement a safe arithmetic chain: parse a string to int, divide by another parsed int, take square root — all with Option.
  • Implement Option::and_then from scratch using only match and show the equivalence.
  • Demonstrate the failure mode: for each step in the chain, show which input causes None to propagate.
  • Write a configuration file traverser using and_then: navigate nested HashMap<String, Value> (where Value is an enum) without panicking.
  • Open Source Repos