Monadic Option Chaining
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
>>= (bind) maps to Rust's Option::and_then>>| (functor map) maps to Rust's Option::map? operator provides ergonomic monadic chaining with explicit control flowand_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
>>= 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.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));
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Bind operator | val (>>=) : 'a option -> ('a -> 'b option) -> 'b option | fn and_then<U>(self, f: impl FnOnce(T) -> Option<U>) -> Option<U> |
| Map operator | val (>>|) : 'a option -> ('a -> 'b) -> 'b option | fn map<U>(self, f: impl FnOnce(T) -> U) -> Option<U> |
| Safe division | val safe_div : int -> int -> int option | fn safe_div(x: i32, y: i32) -> Option<i32> |
| Safe head | val safe_head : 'a list -> 'a option | fn 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.? 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.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.>>= 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
? operator instead of explicit and_then calls and verify the results are identical.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.HashMap<String, Value> structure, write a function get_path(&self, path: &[&str]) -> Option<&Value> using monadic chaining.