292: Option Combinators
Tutorial Video
Text description (accessibility)
This video demonstrates the "292: Option Combinators" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Null pointer errors are the "billion dollar mistake" — unhandled absence of a value causing runtime crashes. Key difference from OCaml: 1. **Naming**: Rust uses `and_then` for monadic bind; OCaml uses `Option.bind` and `let*` syntax.
Tutorial
The Problem
Null pointer errors are the "billion dollar mistake" — unhandled absence of a value causing runtime crashes. Rust's Option<T> encodes optionality in the type system, and its combinator methods enable composing operations on optional values without null checks or sentinel values. This mirrors OCaml's option type and Haskell's Maybe monad — the foundational functional programming approach to nullable values.
🎯 Learning Outcomes
map() to transform Some(T) while passing None through unchangedfilter() to conditionally discard a Some value based on a predicateand_then() — the monadic bind for Optionunwrap_or(), unwrap_or_else(), and unwrap_or_default()Code Example
let doubled = Some(5).map(|x| x * 2);
// Some(10)Key Differences
and_then for monadic bind; OCaml uses Option.bind and let* syntax.Option::filter() directly; OCaml requires Option.bind (fun x -> if pred x then Some x else None).unwrap_or(val) (eager), unwrap_or_else(f) (lazy), unwrap_or_default() (type's Default).Option::zip(other) combines two options into a pair — no OCaml equivalent without manual matching.OCaml Approach
OCaml's Option module provides Option.map, Option.bind, and Option.fold. The let* syntax (OCaml 4.08+) desugars to Option.bind:
let parse_and_sqrt s =
let* x = float_of_string_opt s in
if x >= 0.0 then Some (sqrt x) else None
Full Source
#![allow(clippy::all)]
//! # Option Combinators
//!
//! Work with optional values using `.map()`, `.filter()`, `.and_then()`, and `.unwrap_or()`.
/// Safe square root - returns None for negative inputs
pub fn safe_sqrt(x: f64) -> Option<f64> {
if x >= 0.0 {
Some(x.sqrt())
} else {
None
}
}
/// Parse and compute square root
pub fn parse_and_sqrt(s: &str) -> Option<f64> {
s.parse::<f64>().ok().and_then(safe_sqrt)
}
/// Map an optional value
pub fn double_option(opt: Option<i32>) -> Option<i32> {
opt.map(|x| x * 2)
}
/// Filter by predicate
pub fn filter_even(opt: Option<i32>) -> Option<i32> {
opt.filter(|&x| x % 2 == 0)
}
/// Get with default
pub fn get_or_default(opt: Option<i32>, default: i32) -> i32 {
opt.unwrap_or(default)
}
/// Get with lazy default
pub fn get_or_compute<F>(opt: Option<i32>, f: F) -> i32
where
F: FnOnce() -> i32,
{
opt.unwrap_or_else(f)
}
/// Chain operations
pub fn chain_operations(s: &str) -> Option<i32> {
s.parse::<i32>().ok().filter(|&x| x > 0).map(|x| x * x)
}
/// Use or for fallback Option
pub fn first_valid(a: Option<i32>, b: Option<i32>) -> Option<i32> {
a.or(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_map_some() {
assert_eq!(Some(5i32).map(|x| x * 2), Some(10));
}
#[test]
fn test_map_none() {
assert_eq!(None::<i32>.map(|x| x * 2), None);
}
#[test]
fn test_filter_pass() {
assert_eq!(filter_even(Some(4)), Some(4));
}
#[test]
fn test_filter_fail() {
assert_eq!(filter_even(Some(3)), None);
}
#[test]
fn test_and_then_chain() {
let result = parse_and_sqrt("4.0");
assert!((result.unwrap() - 2.0).abs() < 1e-10);
}
#[test]
fn test_and_then_none() {
assert_eq!(parse_and_sqrt("invalid"), None);
}
#[test]
fn test_and_then_negative() {
assert_eq!(parse_and_sqrt("-4.0"), None);
}
#[test]
fn test_or_default() {
assert_eq!(get_or_default(None, 42), 42);
assert_eq!(get_or_default(Some(10), 42), 10);
}
#[test]
fn test_or() {
assert_eq!(first_valid(None, Some(5)), Some(5));
assert_eq!(first_valid(Some(3), Some(5)), Some(3));
}
#[test]
fn test_chain_operations() {
assert_eq!(chain_operations("5"), Some(25));
assert_eq!(chain_operations("-5"), None);
assert_eq!(chain_operations("abc"), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_map_some() {
assert_eq!(Some(5i32).map(|x| x * 2), Some(10));
}
#[test]
fn test_map_none() {
assert_eq!(None::<i32>.map(|x| x * 2), None);
}
#[test]
fn test_filter_pass() {
assert_eq!(filter_even(Some(4)), Some(4));
}
#[test]
fn test_filter_fail() {
assert_eq!(filter_even(Some(3)), None);
}
#[test]
fn test_and_then_chain() {
let result = parse_and_sqrt("4.0");
assert!((result.unwrap() - 2.0).abs() < 1e-10);
}
#[test]
fn test_and_then_none() {
assert_eq!(parse_and_sqrt("invalid"), None);
}
#[test]
fn test_and_then_negative() {
assert_eq!(parse_and_sqrt("-4.0"), None);
}
#[test]
fn test_or_default() {
assert_eq!(get_or_default(None, 42), 42);
assert_eq!(get_or_default(Some(10), 42), 10);
}
#[test]
fn test_or() {
assert_eq!(first_valid(None, Some(5)), Some(5));
assert_eq!(first_valid(Some(3), Some(5)), Some(3));
}
#[test]
fn test_chain_operations() {
assert_eq!(chain_operations("5"), Some(25));
assert_eq!(chain_operations("-5"), None);
assert_eq!(chain_operations("abc"), None);
}
}
Deep Comparison
OCaml vs Rust: Option Combinators
Pattern 1: Map Some Value
OCaml
let some_5 = Some 5 in
let mapped = Option.map (fun x -> x * 2) some_5
(* Some 10 *)
Rust
let doubled = Some(5).map(|x| x * 2);
// Some(10)
Pattern 2: Chain Optional Operations
OCaml
let safe_div x y = if y = 0 then None else Some (x / y) in
let result = Option.bind some_5 (fun n -> safe_div 10 n)
Rust
fn safe_div(x: i32, y: i32) -> Option<i32> {
if y == 0 { None } else { Some(x / y) }
}
let result = Some(5).and_then(|n| safe_div(10, n));
Pattern 3: Filter by Predicate
OCaml
let even = Option.filter (fun x -> x mod 2 = 0) (Some 6)
(* Some 6 *)
Rust
let even = Some(6).filter(|&x| x % 2 == 0);
// Some(6)
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Map Some | Option.map f opt | opt.map(f) |
| Chain optional | Option.bind opt f | opt.and_then(f) |
| Filter | Option.filter pred opt | opt.filter(pred) |
| Default value | Option.value ~default opt | opt.unwrap_or(default) |
| Lazy fallback | Option.value_or_thunk | opt.unwrap_or_else(f) |
Exercises
Option-returning lookups (user → profile → avatar URL) using and_then(), returning None if any step fails.safe_divide(a: f64, b: f64) -> Option<f64> and compose it with filter(|&x| x.is_finite()) using only combinators.Option<Result<T, E>> and Result<Option<T>, E> using transpose() and verify the two directions invert each other.