โข Option
โข Result
โข 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
โข Option
โข Result
โข 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
// 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 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.
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.
| Concept | OCaml | Rust |
|---|---|---|
| Monadic bind | `Option.bind` / `>>=` operator | `Option::and_then` |
| Do-notation sugar | Not built in (use `let*` in newer OCaml) | `?` operator |
| `None` propagation | Automatic in bind chain | Automatic in `and_then` / `?` |
| Value ownership | Shared (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"
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:
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)
}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 }
})