056 — Result as a Monad
Tutorial
The Problem
Result is a monad: it satisfies the three monad laws (left identity, right identity, associativity) and provides return (wrapping in Ok) and bind (and_then). Recognizing Result as a monad explains why and_then chains and ? feel so clean: they are a principled application of monadic sequencing, the same structure as IO in Haskell, async/await in most languages, and parser combinators.
The monad structure is what makes railway-oriented programming work: the "happy path" is the Ok track, errors get routed to the Err track, and and_then is the switch. Understanding this gives you the vocabulary to recognize and use the same pattern across different types (Option, Future, Iterator).
🎯 Learning Outcomes
Result as a monad with Ok as return and and_then as bindand_then (monadic bind)and_then chaining with the ? operator — both express the same computationResult is a monad but Validation (example 054) is notmap (functor) and and_then (monad) as the complete Result transformation toolkitResult::and_then — the monadic bind that propagates errors automaticallyOk(x).and_then(f) == f(x), right identity r.and_then(Ok) == rCode Example
#![allow(clippy::all)]
// 056: Result as Monad
// Chain fallible operations with and_then and ?
use std::num::ParseIntError;
#[derive(Debug, PartialEq)]
enum CalcError {
Parse(String),
DivByZero,
}
fn parse_int(s: &str) -> Result<i32, CalcError> {
s.parse::<i32>()
.map_err(|e| CalcError::Parse(e.to_string()))
}
fn safe_div(a: i32, b: i32) -> Result<i32, CalcError> {
if b == 0 {
Err(CalcError::DivByZero)
} else {
Ok(a / b)
}
}
// Approach 1: Using and_then (monadic bind)
fn compute_bind(s1: &str, s2: &str) -> Result<i32, CalcError> {
parse_int(s1).and_then(|a| parse_int(s2).and_then(|b| safe_div(a, b)))
}
// Approach 2: Using ? operator (syntactic sugar for bind)
fn compute_question(s1: &str, s2: &str) -> Result<i32, CalcError> {
let a = parse_int(s1)?;
let b = parse_int(s2)?;
safe_div(a, b)
}
// Approach 3: Chained pipeline
fn pipeline(s: &str) -> Result<i32, CalcError> {
parse_int(s)
.and_then(|n| safe_div(n, 2))
.map(|n| n + 1)
.map(|n| n * 2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_int() {
assert_eq!(parse_int("42"), Ok(42));
assert!(parse_int("abc").is_err());
}
#[test]
fn test_compute_bind() {
assert_eq!(compute_bind("10", "3"), Ok(3));
assert_eq!(compute_bind("10", "0"), Err(CalcError::DivByZero));
assert!(compute_bind("abc", "3").is_err());
}
#[test]
fn test_compute_question() {
assert_eq!(compute_question("10", "3"), Ok(3));
assert_eq!(compute_question("10", "0"), Err(CalcError::DivByZero));
}
#[test]
fn test_pipeline() {
assert_eq!(pipeline("10"), Ok(12));
assert!(pipeline("abc").is_err());
}
}Key Differences
>>= vs and_then**: Haskell uses >>= as the bind operator. OCaml defines it by convention. Rust uses the method name and_then. All three are the same monad operation.Ok(a).and_then(f) == f(a). Right identity: r.and_then(Ok) == r. Associativity: r.and_then(f).and_then(g) == r.and_then(|x| f(x).and_then(g)). Verify these in tests.? ergonomics**: Rust's ? makes the monad pattern syntactically cheap — writing monadic code feels like imperative code. OCaml's let* achieves the same goal.and_then chains require a consistent error type E. Use map_err to normalize before chaining, or use Box<dyn Error> for heterogeneous chains.Result as a monad:** Ok(x).and_then(f) = f(x) and Err(e).and_then(f) = Err(e). These are the monad laws for Result (return + bind). Verifying monad laws in tests builds confidence in error handling correctness.? as do-notation:** Haskell's do { x <- action; ... } is equivalent to Rust's let x = action?. Both are syntactic sugar for monadic bind.Result monad is sequencing: each step feeds its output to the next, and a single failure short-circuits the chain. No explicit error checking between steps.let*:** With ppx_let, OCaml supports let* x = fallible () in next x as syntactic sugar for Result.bind (fallible ()) (fun x -> next x). Equivalent to Rust's ?.OCaml Approach
OCaml's result monad: let ( >>= ) r f = Result.bind r f. Then: parse_int s1 >>= fun a -> parse_int s2 >>= fun b -> safe_div a b. With let* (ppx_let): let* a = parse_int s1 in let* b = parse_int s2 in safe_div a b. Both forms are equivalent. OCaml's >>= operator for result is not in stdlib but is easily defined and widely used.
Full Source
#![allow(clippy::all)]
// 056: Result as Monad
// Chain fallible operations with and_then and ?
use std::num::ParseIntError;
#[derive(Debug, PartialEq)]
enum CalcError {
Parse(String),
DivByZero,
}
fn parse_int(s: &str) -> Result<i32, CalcError> {
s.parse::<i32>()
.map_err(|e| CalcError::Parse(e.to_string()))
}
fn safe_div(a: i32, b: i32) -> Result<i32, CalcError> {
if b == 0 {
Err(CalcError::DivByZero)
} else {
Ok(a / b)
}
}
// Approach 1: Using and_then (monadic bind)
fn compute_bind(s1: &str, s2: &str) -> Result<i32, CalcError> {
parse_int(s1).and_then(|a| parse_int(s2).and_then(|b| safe_div(a, b)))
}
// Approach 2: Using ? operator (syntactic sugar for bind)
fn compute_question(s1: &str, s2: &str) -> Result<i32, CalcError> {
let a = parse_int(s1)?;
let b = parse_int(s2)?;
safe_div(a, b)
}
// Approach 3: Chained pipeline
fn pipeline(s: &str) -> Result<i32, CalcError> {
parse_int(s)
.and_then(|n| safe_div(n, 2))
.map(|n| n + 1)
.map(|n| n * 2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_int() {
assert_eq!(parse_int("42"), Ok(42));
assert!(parse_int("abc").is_err());
}
#[test]
fn test_compute_bind() {
assert_eq!(compute_bind("10", "3"), Ok(3));
assert_eq!(compute_bind("10", "0"), Err(CalcError::DivByZero));
assert!(compute_bind("abc", "3").is_err());
}
#[test]
fn test_compute_question() {
assert_eq!(compute_question("10", "3"), Ok(3));
assert_eq!(compute_question("10", "0"), Err(CalcError::DivByZero));
}
#[test]
fn test_pipeline() {
assert_eq!(pipeline("10"), Ok(12));
assert!(pipeline("abc").is_err());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_int() {
assert_eq!(parse_int("42"), Ok(42));
assert!(parse_int("abc").is_err());
}
#[test]
fn test_compute_bind() {
assert_eq!(compute_bind("10", "3"), Ok(3));
assert_eq!(compute_bind("10", "0"), Err(CalcError::DivByZero));
assert!(compute_bind("abc", "3").is_err());
}
#[test]
fn test_compute_question() {
assert_eq!(compute_question("10", "3"), Ok(3));
assert_eq!(compute_question("10", "0"), Err(CalcError::DivByZero));
}
#[test]
fn test_pipeline() {
assert_eq!(pipeline("10"), Ok(12));
assert!(pipeline("abc").is_err());
}
}
Deep Comparison
Core Insight
A monad is a type with return (wrap a value) and bind (chain operations that may fail). Result is a monad: Ok is return, and_then/bind chains fallible operations, short-circuiting on Err.
OCaml Approach
Result.bind result f — chains fallible functionsResult.map result f — transforms the Ok valuelet* syntax with binding operators (OCaml 4.08+)Rust Approach
.and_then(f) — monadic bind.map(f) — functor map? operator — desugar to match + early returnComparison Table
| Operation | OCaml | Rust |
|---|---|---|
| Return/wrap | Ok x | Ok(x) |
| Bind | Result.bind r f | r.and_then(f) |
| Map | Result.map f r | r.map(f) |
| Sugar | let* x = r in ... | let x = r?; |
| Short-circuit | Pattern match | ? operator |
Exercises
Result<i32, String>. For each law, construct a concrete case and assert equality.State<S, T> type wrapping impl Fn(S) -> (T, S). Implement and_then for it. Show how it enables stateful computation in a pure functional style.type Cont<R, T> = Box<dyn FnOnce(Box<dyn FnOnce(T) -> R>) -> R>. Define bind and use it to express error handling in continuation-passing style (connects to example 099).kleisli_compose<A, B, C, E>(f: impl Fn(A) -> Result<B, E>, g: impl Fn(B) -> Result<C, E>) -> impl Fn(A) -> Result<C, E> — the "fish operator" >=> from Haskell.or_else_result<T, E>(r: Result<T, E>, alternative: impl FnOnce(E) -> Result<T, E>) -> Result<T, E> — the MonadPlus recovery operation for Result.