ExamplesBy LevelBy TopicLearning Paths
258 Intermediate

Monadic Option Chaining

Monadic patterns

Tutorial Video

Text description (accessibility)

This video demonstrates the "Monadic Option Chaining" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Monadic patterns. Chain multiple partial functions (functions returning `Option`) so that a `None` at any step short-circuits the entire computation, without nesting `match` expressions. Key difference from OCaml: 1. **Operator syntax:** OCaml defines `>>=` as an infix operator; Rust uses method syntax `and_then` and `map` (no custom operators in stable Rust).

Tutorial

The Problem

Chain multiple partial functions (functions returning Option) so that a None at any step short-circuits the entire computation, without nesting match expressions.

🎯 Learning Outcomes

  • • How OCaml's >>= (bind) maps to Rust's Option::and_then
  • • How OCaml's >>| (functor map) maps to Rust's Option::map
  • • How Rust's ? operator provides ergonomic monadic chaining with explicit control flow
  • • Why and_then composes better than nested match for sequential fallible operations
  • 🦀 The Rust Way

    Rust's Option<T> has and_then (monadic bind) and map (functor map) built into the standard library, so no operator definitions are needed. The ? operator offers a third style: it desugars to early return on None, making control flow explicit while keeping code concise. All three styles produce identical semantics.

    Code Example

    pub fn safe_div(x: i32, y: i32) -> Option<i32> {
        if y == 0 { None } else { Some(x / y) }
    }
    
    pub fn safe_head(list: &[i32]) -> Option<i32> {
        list.first().copied()
    }
    
    pub fn compute_idiomatic(lst: &[i32]) -> Option<i32> {
        safe_head(lst)
            .and_then(|x| safe_div(100, x))
            .map(|r| r * 2)
    }

    Key Differences

  • Operator syntax: OCaml defines >>= as an infix operator; Rust uses method syntax and_then and map (no custom operators in stable Rust).
  • **? operator:** Rust's ? is a unique construct with no OCaml equivalent — it makes monadic short-circuiting look like imperative early return.
  • **Option::bind in stdlib:** OCaml 4.08+ added Option.bind; before that, developers always defined >>= manually. Rust has had and_then from day one.
  • Ownership: Rust's and_then and map consume the Option by value; closures receive owned T, not a reference, matching OCaml's value semantics.
  • OCaml Approach

    OCaml defines custom infix operators >>= and >>| to chain Option values in a pipeline style. This is the option monad: >>= sequences computations that may fail, >>| transforms successful values. The result reads left-to-right and each None silently terminates the chain.

    Full Source

    #![allow(clippy::all)]
    // Solution 1: Idiomatic Rust — Option's built-in monadic combinators
    // `and_then` is Rust's bind (>>=), `map` is Rust's fmap (>>|)
    pub fn safe_div(x: i32, y: i32) -> Option<i32> {
        if y == 0 {
            None
        } else {
            Some(x / y)
        }
    }
    
    // Takes &[i32] — borrows the slice, no allocation needed
    pub fn safe_head(list: &[i32]) -> Option<i32> {
        list.first().copied()
    }
    
    pub fn compute_idiomatic(lst: &[i32]) -> Option<i32> {
        safe_head(lst).and_then(|x| safe_div(100, x)).map(|r| r * 2)
    }
    
    // Solution 2: Explicit monadic bind — mirrors OCaml's >>= operator
    // Demonstrates what and_then desugars to. Note: >>| (fmap) IS Option::map.
    fn bind<T, U>(opt: Option<T>, f: impl FnOnce(T) -> Option<U>) -> Option<U> {
        match opt {
            None => None,
            Some(x) => f(x),
        }
    }
    
    pub fn compute_explicit(lst: &[i32]) -> Option<i32> {
        let divided = bind(safe_head(lst), |x| safe_div(100, x));
        divided.map(|r| r * 2) // >>| is just Option::map
    }
    
    // Solution 3: Using the `?` operator — Rust's ergonomic monadic shorthand
    // `?` early-returns None if the value is None, like >>= but with explicit control flow
    pub fn compute_question_mark(lst: &[i32]) -> Option<i32> {
        let x = safe_head(lst)?;
        let r = safe_div(100, x)?;
        Some(r * 2)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_normal_case_all_approaches() {
            let lst = &[5, 3, 1];
            // 100 / 5 = 20, 20 * 2 = 40
            assert_eq!(compute_idiomatic(lst), Some(40));
            assert_eq!(compute_explicit(lst), Some(40));
            assert_eq!(compute_question_mark(lst), Some(40));
        }
    
        #[test]
        fn test_division_by_zero_propagates_none() {
            let lst = &[0, 1];
            // safe_div(100, 0) => None, propagates
            assert_eq!(compute_idiomatic(lst), None);
            assert_eq!(compute_explicit(lst), None);
            assert_eq!(compute_question_mark(lst), None);
        }
    
        #[test]
        fn test_empty_list_propagates_none() {
            let lst: &[i32] = &[];
            // safe_head([]) => None, propagates
            assert_eq!(compute_idiomatic(lst), None);
            assert_eq!(compute_explicit(lst), None);
            assert_eq!(compute_question_mark(lst), None);
        }
    
        #[test]
        fn test_single_element_list() {
            // 100 / 4 = 25, 25 * 2 = 50
            assert_eq!(compute_idiomatic(&[4]), Some(50));
            assert_eq!(compute_explicit(&[4]), Some(50));
            assert_eq!(compute_question_mark(&[4]), Some(50));
        }
    
        #[test]
        fn test_safe_div_nonzero() {
            assert_eq!(safe_div(100, 5), Some(20));
            assert_eq!(safe_div(7, 3), Some(2)); // integer division
        }
    
        #[test]
        fn test_safe_div_by_zero() {
            assert_eq!(safe_div(100, 0), None);
            assert_eq!(safe_div(0, 0), None);
        }
    
        #[test]
        fn test_safe_head() {
            assert_eq!(safe_head(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head(&[42]), Some(42));
            assert_eq!(safe_head(&[]), None);
        }
    
        #[test]
        fn test_negative_head_element() {
            // safe_div(100, -4) = -25, -25 * 2 = -50
            assert_eq!(compute_idiomatic(&[-4, 1]), Some(-50));
            assert_eq!(compute_explicit(&[-4, 1]), Some(-50));
            assert_eq!(compute_question_mark(&[-4, 1]), Some(-50));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_normal_case_all_approaches() {
            let lst = &[5, 3, 1];
            // 100 / 5 = 20, 20 * 2 = 40
            assert_eq!(compute_idiomatic(lst), Some(40));
            assert_eq!(compute_explicit(lst), Some(40));
            assert_eq!(compute_question_mark(lst), Some(40));
        }
    
        #[test]
        fn test_division_by_zero_propagates_none() {
            let lst = &[0, 1];
            // safe_div(100, 0) => None, propagates
            assert_eq!(compute_idiomatic(lst), None);
            assert_eq!(compute_explicit(lst), None);
            assert_eq!(compute_question_mark(lst), None);
        }
    
        #[test]
        fn test_empty_list_propagates_none() {
            let lst: &[i32] = &[];
            // safe_head([]) => None, propagates
            assert_eq!(compute_idiomatic(lst), None);
            assert_eq!(compute_explicit(lst), None);
            assert_eq!(compute_question_mark(lst), None);
        }
    
        #[test]
        fn test_single_element_list() {
            // 100 / 4 = 25, 25 * 2 = 50
            assert_eq!(compute_idiomatic(&[4]), Some(50));
            assert_eq!(compute_explicit(&[4]), Some(50));
            assert_eq!(compute_question_mark(&[4]), Some(50));
        }
    
        #[test]
        fn test_safe_div_nonzero() {
            assert_eq!(safe_div(100, 5), Some(20));
            assert_eq!(safe_div(7, 3), Some(2)); // integer division
        }
    
        #[test]
        fn test_safe_div_by_zero() {
            assert_eq!(safe_div(100, 0), None);
            assert_eq!(safe_div(0, 0), None);
        }
    
        #[test]
        fn test_safe_head() {
            assert_eq!(safe_head(&[1, 2, 3]), Some(1));
            assert_eq!(safe_head(&[42]), Some(42));
            assert_eq!(safe_head(&[]), None);
        }
    
        #[test]
        fn test_negative_head_element() {
            // safe_div(100, -4) = -25, -25 * 2 = -50
            assert_eq!(compute_idiomatic(&[-4, 1]), Some(-50));
            assert_eq!(compute_explicit(&[-4, 1]), Some(-50));
            assert_eq!(compute_question_mark(&[-4, 1]), Some(-50));
        }
    }

    Deep Comparison

    OCaml vs Rust: Monadic Option Chaining

    Side-by-Side Code

    OCaml

    let ( >>= ) opt f = match opt with
      | None -> None
      | Some x -> f x
    
    let ( >>| ) opt f = match opt with
      | None -> None
      | Some x -> Some (f x)
    
    let safe_div x y = if y = 0 then None else Some (x / y)
    let safe_head = function [] -> None | h :: _ -> Some h
    
    let compute lst =
      safe_head lst >>= fun x ->
      safe_div 100 x >>| fun r ->
      r * 2
    

    Rust (idiomatic)

    pub fn safe_div(x: i32, y: i32) -> Option<i32> {
        if y == 0 { None } else { Some(x / y) }
    }
    
    pub fn safe_head(list: &[i32]) -> Option<i32> {
        list.first().copied()
    }
    
    pub fn compute_idiomatic(lst: &[i32]) -> Option<i32> {
        safe_head(lst)
            .and_then(|x| safe_div(100, x))
            .map(|r| r * 2)
    }
    

    Rust (explicit bind — shows the desugaring)

    fn bind<T, U>(opt: Option<T>, f: impl FnOnce(T) -> Option<U>) -> Option<U> {
        match opt {
            None => None,
            Some(x) => f(x),
        }
    }
    
    pub fn compute_explicit(lst: &[i32]) -> Option<i32> {
        let divided = bind(safe_head(lst), |x| safe_div(100, x));
        divided.map(|r| r * 2)
    }
    

    Rust (question-mark operator)

    pub fn compute_question_mark(lst: &[i32]) -> Option<i32> {
        let x = safe_head(lst)?;
        let r = safe_div(100, x)?;
        Some(r * 2)
    }
    

    Type Signatures

    ConceptOCamlRust
    Bind operatorval (>>=) : 'a option -> ('a -> 'b option) -> 'b optionfn and_then<U>(self, f: impl FnOnce(T) -> Option<U>) -> Option<U>
    Map operatorval (>>|) : 'a option -> ('a -> 'b) -> 'b optionfn map<U>(self, f: impl FnOnce(T) -> U) -> Option<U>
    Safe divisionval safe_div : int -> int -> int optionfn safe_div(x: i32, y: i32) -> Option<i32>
    Safe headval safe_head : 'a list -> 'a optionfn safe_head(list: &[i32]) -> Option<i32>

    Key Insights

  • **>>= is and_then:** OCaml's custom bind operator and Rust's Option::and_then are identical in semantics — both propagate None and apply f to the inner value when Some. Rust provides this in the standard library; OCaml developers historically defined it themselves.
  • **>>| is Option::map:** The functor-map operator in OCaml is exactly Option::map in Rust. Clippy will even reject a manual Rust implementation of fmap with match, telling you to use .map() instead — confirming this identity.
  • **The ? operator desugars to bind:** Rust's ? on an Option is syntactic sugar for "return None early if None, otherwise unwrap". This is monadic short-circuit with imperative-style syntax, unique to Rust and without a direct OCaml equivalent.
  • Value ownership vs. reference semantics: Rust's and_then and map consume the Option by value, and the closures receive owned T. OCaml also passes values, but without explicit ownership tracking. In Rust, using .copied() on Option<&T> to produce Option<T> is the idiomatic way to decouple borrowing from the chain.
  • No user-defined infix operators in stable Rust: OCaml makes it natural to define >>= as an infix operator. Rust does not support custom infix operators in stable code, so the chaining reads as method calls. This is a deliberate Rust design decision for readability and tooling.
  • When to Use Each Style

    **Use and_then + map when:** building a pipeline with multiple fallible steps that reads cleanly left-to-right — the method chain style makes the data flow obvious and composes well with iterator chains.

    **Use ? when:** writing code that resembles sequential imperative steps, or when intermediate values need to be named and reused. The ? style is easier for developers unfamiliar with monadic thinking to read and debug.

    **Use explicit bind (match) when:** teaching or documenting what monadic chaining means under the hood, or when porting OCaml code directly for comparison purposes.

    Exercises

  • Rewrite the same computation chain using the ? operator instead of explicit and_then calls and verify the results are identical.
  • Implement option_all that takes a Vec<Option<T>> and returns Some(Vec<T>) only if every element is Some, using a fold over the sequence.
  • Build a small JSON-like path traversal: given a nested HashMap<String, Value> structure, write a function get_path(&self, path: &[&str]) -> Option<&Value> using monadic chaining.
  • Open Source Repos