Option Monad
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
and_then (monadic bind): if Some(x), apply f to x and return f(x); if None, return Noneand_then calls to build pipelines of fallible lookups? operator as syntactic sugar for and_then (or return Err())map: map wraps the result; and_then expects f to return Option<U>Code Example
safe_div(100, 4)
.and_then(|q| safe_sqrt(q))
.map(|r| r as i32)Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Bind function | Option::and_then | Option.bind |
| Infix operator | ? in fn -> Option<T> | >>= via let ( >>= ) |
| Do notation | ? operator | let%bind with ppx_let |
| Map vs bind | map for T -> U, and_then for T -> Option<U> | Option.map vs Option.bind |
| None propagation | Automatic via and_then | Same |
| Chain length | Unlimited and_then chain | Same |
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));
}
}#[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
find_user_docs using the ? operator inside a function returning Option<String> and verify same behavior.Option::and_then from scratch using only match and show the equivalence.None to propagate.and_then: navigate nested HashMap<String, Value> (where Value is an enum) without panicking.