004 — Option and Result
Tutorial
The Problem
Null references were famously called "the billion-dollar mistake" by their inventor Tony Hoare. Languages like Java, C, and C++ use null/NULL to represent missing values, which causes NullPointerException, segfaults, and unchecked error codes at runtime. Functional languages solved this with algebraic types: Option (sometimes called Maybe) wraps a value that may or may not exist, and Result (or Either) wraps a value that may have failed with an error.
These types make the possibility of absence or failure explicit in the type signature, forcing callers to handle both cases. They also compose via map and and_then (monadic bind), enabling clean pipelines of fallible operations without nested if-let chains.
🎯 Learning Outcomes
Option<T> for values that may not exist, avoiding nullResult<T, E> for operations that may fail with a typed error.map() and .and_then() to avoid nested matchingand_then is "do this next, but only if the previous step succeeded"Option and Result with .ok_or() and .ok()if let Some(x) = opt { ... } as shorthand when only the Some case needs handling, without writing a full matchCode Example
#![allow(clippy::all)]
// 004: Option and Result
// Safe handling of missing values and errors
// Approach 1: Option basics
fn safe_div(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn safe_head(v: &[i32]) -> Option<i32> {
v.first().copied()
}
fn find_even(v: &[i32]) -> Option<i32> {
v.iter().find(|&&x| x % 2 == 0).copied()
}
// Approach 2: Chaining with map and and_then
fn double_head(v: &[i32]) -> Option<i32> {
safe_head(v).map(|x| x * 2)
}
fn safe_div_then_add(a: i32, b: i32, c: i32) -> Option<i32> {
safe_div(a, b).map(|q| q + c)
}
fn chain_lookups(v1: &[i32], v2: &[i32]) -> Option<i32> {
safe_head(v1).and_then(|idx| v2.get(idx as usize).copied())
}
// Approach 3: Result for richer errors
#[derive(Debug, PartialEq)]
enum MyError {
DivByZero,
NegativeInput,
EmptyList,
}
fn safe_div_r(a: i32, b: i32) -> Result<i32, MyError> {
if b == 0 {
Err(MyError::DivByZero)
} else {
Ok(a / b)
}
}
fn safe_sqrt(x: f64) -> Result<f64, MyError> {
if x < 0.0 {
Err(MyError::NegativeInput)
} else {
Ok(x.sqrt())
}
}
fn safe_head_r(v: &[i32]) -> Result<i32, MyError> {
v.first().copied().ok_or(MyError::EmptyList)
}
fn compute(v: &[i32]) -> Result<i32, MyError> {
let x = safe_head_r(v)?;
safe_div_r(x * 10, 3)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10, 3), Some(3));
assert_eq!(safe_div(10, 0), None);
}
#[test]
fn test_safe_head() {
assert_eq!(safe_head(&[1, 2, 3]), Some(1));
assert_eq!(safe_head(&[]), None);
}
#[test]
fn test_find_even() {
assert_eq!(find_even(&[1, 3, 4, 5]), Some(4));
assert_eq!(find_even(&[1, 3, 5]), None);
}
#[test]
fn test_double_head() {
assert_eq!(double_head(&[5, 10]), Some(10));
assert_eq!(double_head(&[]), None);
}
#[test]
fn test_chain_lookups() {
assert_eq!(chain_lookups(&[1], &[10, 20, 30]), Some(20));
assert_eq!(chain_lookups(&[], &[10, 20]), None);
}
#[test]
fn test_result() {
assert_eq!(safe_div_r(10, 2), Ok(5));
assert_eq!(safe_div_r(10, 0), Err(MyError::DivByZero));
}
#[test]
fn test_compute() {
assert_eq!(compute(&[5, 10]), Ok(16));
assert_eq!(compute(&[]), Err(MyError::EmptyList));
}
}Key Differences
? operator**: Rust's ? in a function returning Result is syntactic sugar for and_then/early return. OCaml uses let* (ppx_let) or explicit match — ? does not exist in standard OCaml.Result<T, E> is generic over the error type. OCaml's result type is also ('a, 'b) result but idiomatic OCaml often uses polymorphic variants for errors.ok_or**: Rust provides .ok_or(err) to convert Option<T> to Result<T, E>. OCaml uses Option.to_result ~none:err..unwrap(), OCaml: Option.get). Both should be avoided in production code.Option forces explicit handling of absence; there is no way to call a method on None by accident.? operator:** Rust's ? for early-return on error has no direct OCaml syntax equivalent (OCaml uses let* with ppx_let or explicit match). Both achieve the same monadic composition.Result<T, E> carries a typed error E. OCaml's result type: type ('a, 'b) result = Ok of 'a | Error of 'b. Both are isomorphic..ok() (Result → Option), .ok_or(e) (Option → Result). OCaml uses manual match for these conversions.OCaml Approach
OCaml's option type (None | Some x) and result type (Ok x | Error e) work identically. Option.map and Option.bind correspond to Rust's .map() and .and_then(). The |> pipe makes chaining natural: safe_head lst |> Option.bind (fun idx -> ...). OCaml also has let* (monadic let) for sequential binding without nesting: let* x = safe_div a b in let* y = safe_sqrt x in Ok (x + y).
Full Source
#![allow(clippy::all)]
// 004: Option and Result
// Safe handling of missing values and errors
// Approach 1: Option basics
fn safe_div(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn safe_head(v: &[i32]) -> Option<i32> {
v.first().copied()
}
fn find_even(v: &[i32]) -> Option<i32> {
v.iter().find(|&&x| x % 2 == 0).copied()
}
// Approach 2: Chaining with map and and_then
fn double_head(v: &[i32]) -> Option<i32> {
safe_head(v).map(|x| x * 2)
}
fn safe_div_then_add(a: i32, b: i32, c: i32) -> Option<i32> {
safe_div(a, b).map(|q| q + c)
}
fn chain_lookups(v1: &[i32], v2: &[i32]) -> Option<i32> {
safe_head(v1).and_then(|idx| v2.get(idx as usize).copied())
}
// Approach 3: Result for richer errors
#[derive(Debug, PartialEq)]
enum MyError {
DivByZero,
NegativeInput,
EmptyList,
}
fn safe_div_r(a: i32, b: i32) -> Result<i32, MyError> {
if b == 0 {
Err(MyError::DivByZero)
} else {
Ok(a / b)
}
}
fn safe_sqrt(x: f64) -> Result<f64, MyError> {
if x < 0.0 {
Err(MyError::NegativeInput)
} else {
Ok(x.sqrt())
}
}
fn safe_head_r(v: &[i32]) -> Result<i32, MyError> {
v.first().copied().ok_or(MyError::EmptyList)
}
fn compute(v: &[i32]) -> Result<i32, MyError> {
let x = safe_head_r(v)?;
safe_div_r(x * 10, 3)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10, 3), Some(3));
assert_eq!(safe_div(10, 0), None);
}
#[test]
fn test_safe_head() {
assert_eq!(safe_head(&[1, 2, 3]), Some(1));
assert_eq!(safe_head(&[]), None);
}
#[test]
fn test_find_even() {
assert_eq!(find_even(&[1, 3, 4, 5]), Some(4));
assert_eq!(find_even(&[1, 3, 5]), None);
}
#[test]
fn test_double_head() {
assert_eq!(double_head(&[5, 10]), Some(10));
assert_eq!(double_head(&[]), None);
}
#[test]
fn test_chain_lookups() {
assert_eq!(chain_lookups(&[1], &[10, 20, 30]), Some(20));
assert_eq!(chain_lookups(&[], &[10, 20]), None);
}
#[test]
fn test_result() {
assert_eq!(safe_div_r(10, 2), Ok(5));
assert_eq!(safe_div_r(10, 0), Err(MyError::DivByZero));
}
#[test]
fn test_compute() {
assert_eq!(compute(&[5, 10]), Ok(16));
assert_eq!(compute(&[]), Err(MyError::EmptyList));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10, 3), Some(3));
assert_eq!(safe_div(10, 0), None);
}
#[test]
fn test_safe_head() {
assert_eq!(safe_head(&[1, 2, 3]), Some(1));
assert_eq!(safe_head(&[]), None);
}
#[test]
fn test_find_even() {
assert_eq!(find_even(&[1, 3, 4, 5]), Some(4));
assert_eq!(find_even(&[1, 3, 5]), None);
}
#[test]
fn test_double_head() {
assert_eq!(double_head(&[5, 10]), Some(10));
assert_eq!(double_head(&[]), None);
}
#[test]
fn test_chain_lookups() {
assert_eq!(chain_lookups(&[1], &[10, 20, 30]), Some(20));
assert_eq!(chain_lookups(&[], &[10, 20]), None);
}
#[test]
fn test_result() {
assert_eq!(safe_div_r(10, 2), Ok(5));
assert_eq!(safe_div_r(10, 0), Err(MyError::DivByZero));
}
#[test]
fn test_compute() {
assert_eq!(compute(&[5, 10]), Ok(16));
assert_eq!(compute(&[]), Err(MyError::EmptyList));
}
}
Deep Comparison
Core Insight
Option replaces null pointers. Result replaces exceptions. Both languages encode success/failure in the type system, forcing the caller to handle every case.
OCaml Approach
option type: Some x | Noneresult type: Ok x | Error eOption.map, Option.bind for chainingResult.map, Result.bind for chainingRust Approach
Option<T>: Some(x) / NoneResult<T, E>: Ok(x) / Err(e).map(), .and_then() for chaining? operator for early return on error.unwrap_or(), .unwrap_or_else() for defaultsComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Option type | 'a option | Option<T> |
| Result type | ('a, 'e) result | Result<T, E> |
| Map | Option.map f o | o.map(f) |
| Bind/FlatMap | Option.bind o f | o.and_then(f) |
| Default | Option.value ~default o | o.unwrap_or(d) |
| Error propagation | Pattern match | ? operator |
Exercises
safe_get(v: &[i32], i: usize) -> Option<i32> and chain it with safe_div to implement divide_at_index(nums: &[i32], i: usize, divisor: i32) -> Option<i32>.all_or_none(opts: &[Option<i32>]) -> Option<Vec<i32>> that returns Some only if all inputs are Some, using .collect::<Option<Vec<_>>>().Result<i32, String> with a descriptive error message at each step.User { address: Option<Address> } where Address { city: Option<String> }, write a function that returns the city as Option<&str> using and_then and as_deref.sequence(opts: Vec<Option<T>>) -> Option<Vec<T>> that returns None if any element is None, otherwise returns Some of all the values — equivalent to OCaml's Option.join applied to a list.