Result Monad
Tutorial
The Problem
Error handling with match on every Result is verbose and obscures the happy path. The Result monad — Rust's Result::and_then and the ? operator — chains fallible operations so that the first error short-circuits the chain and is returned immediately. This is the foundation of Rust's ergonomic error handling: parse()?.compute()?.serialize()? reads like a straight pipeline yet properly propagates errors. The same pattern is OCaml's Result.bind, Haskell's Either monad, and Scala's for-comprehensions over Either. Understanding it as a monad explains why map and and_then behave differently and how to write clean, composable error-handling code.
🎯 Learning Outcomes
and_then for Result: if Ok(x), apply f(x) returning Result<U, E>; if Err(e), return Err(e)? as syntactic sugar: expr? = match expr { Ok(x) => x, Err(e) => return Err(e.into()) }and_then calls to build pipelines that propagate errors without nested matchesmap (transform Ok value, keep same error type) from and_then (can return new Err)Code Example
fn validate_input(s: &str) -> Result<i32, String> {
parse_int(s)
.and_then(check_positive)
.and_then(check_even)
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Bind | Result::and_then | Result.bind |
| Syntax sugar | ? operator | let* (ppx_let or 4.08+) |
| Error conversion | map_err / From trait | Result.map_error |
| Error propagation | ? calls .into() for type conversion | Manual Result.map_error |
| Ok path | Right-bias | Right-bias |
| Multiple error types | Box<dyn Error> or anyhow | string or polymorphic variant |
OCaml Approach
OCaml's Result.bind has the signature ('a, 'e) result -> ('a -> ('b, 'e) result) -> ('b, 'e) result. The infix let ( >>= ) = Result.bind enables pipelines. OCaml's let open Result in parse_int s >>= double_if_positive reads naturally. The let* x = parse_int s in double_if_positive x syntax (with ppx_let or OCaml 4.08+) provides do-notation. Result.map_error converts error types, mirroring Rust's map_err.
Full Source
#![allow(clippy::all)]
// Example 056: Result Monad
// Result monad: chain computations that may fail with error info
// Approach 1: and_then chains
fn parse_int(s: &str) -> Result<i32, String> {
s.parse::<i32>()
.map_err(|_| format!("Not an integer: {}", s))
}
fn check_positive(n: i32) -> Result<i32, String> {
if n > 0 {
Ok(n)
} else {
Err(format!("Not positive: {}", n))
}
}
fn check_even(n: i32) -> Result<i32, String> {
if n % 2 == 0 {
Ok(n)
} else {
Err(format!("Not even: {}", n))
}
}
fn validate_input(s: &str) -> Result<i32, String> {
parse_int(s).and_then(check_positive).and_then(check_even)
}
// Approach 2: Using ? operator (Rust's monadic do-notation)
fn validate_input_question(s: &str) -> Result<i32, String> {
let n = parse_int(s)?;
let n = check_positive(n)?;
let n = check_even(n)?;
Ok(n)
}
// Approach 3: Map and bind combined
fn double_validated(s: &str) -> Result<i32, String> {
validate_input(s).map(|n| n * 2)
}
// Bonus: custom error type with From for automatic ? conversion
#[derive(Debug, PartialEq)]
enum ValidationError {
ParseError(String),
NotPositive(i32),
NotEven(i32),
}
fn parse_int_typed(s: &str) -> Result<i32, ValidationError> {
s.parse::<i32>()
.map_err(|_| ValidationError::ParseError(s.to_string()))
}
fn check_positive_typed(n: i32) -> Result<i32, ValidationError> {
if n > 0 {
Ok(n)
} else {
Err(ValidationError::NotPositive(n))
}
}
fn check_even_typed(n: i32) -> Result<i32, ValidationError> {
if n % 2 == 0 {
Ok(n)
} else {
Err(ValidationError::NotEven(n))
}
}
fn validate_typed(s: &str) -> Result<i32, ValidationError> {
let n = parse_int_typed(s)?;
let n = check_positive_typed(n)?;
check_even_typed(n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_input() {
assert_eq!(validate_input("42"), Ok(42));
}
#[test]
fn test_parse_error() {
assert_eq!(validate_input("hello"), Err("Not an integer: hello".into()));
}
#[test]
fn test_not_positive() {
assert_eq!(validate_input("-4"), Err("Not positive: -4".into()));
}
#[test]
fn test_not_even() {
assert_eq!(validate_input("7"), Err("Not even: 7".into()));
}
#[test]
fn test_question_mark_same_as_and_then() {
for s in &["42", "hello", "-4", "7"] {
assert_eq!(validate_input(s), validate_input_question(s));
}
}
#[test]
fn test_double() {
assert_eq!(double_validated("42"), Ok(84));
}
#[test]
fn test_typed_errors() {
assert_eq!(validate_typed("42"), Ok(42));
assert_eq!(
validate_typed("bad"),
Err(ValidationError::ParseError("bad".into()))
);
assert_eq!(validate_typed("-2"), Err(ValidationError::NotPositive(-2)));
assert_eq!(validate_typed("3"), Err(ValidationError::NotEven(3)));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_input() {
assert_eq!(validate_input("42"), Ok(42));
}
#[test]
fn test_parse_error() {
assert_eq!(validate_input("hello"), Err("Not an integer: hello".into()));
}
#[test]
fn test_not_positive() {
assert_eq!(validate_input("-4"), Err("Not positive: -4".into()));
}
#[test]
fn test_not_even() {
assert_eq!(validate_input("7"), Err("Not even: 7".into()));
}
#[test]
fn test_question_mark_same_as_and_then() {
for s in &["42", "hello", "-4", "7"] {
assert_eq!(validate_input(s), validate_input_question(s));
}
}
#[test]
fn test_double() {
assert_eq!(double_validated("42"), Ok(84));
}
#[test]
fn test_typed_errors() {
assert_eq!(validate_typed("42"), Ok(42));
assert_eq!(
validate_typed("bad"),
Err(ValidationError::ParseError("bad".into()))
);
assert_eq!(validate_typed("-2"), Err(ValidationError::NotPositive(-2)));
assert_eq!(validate_typed("3"), Err(ValidationError::NotEven(3)));
}
}
Deep Comparison
Comparison: Result Monad
Bind Chain
OCaml:
let validate_input s =
parse_int s >>= check_positive >>= check_even
Rust:
fn validate_input(s: &str) -> Result<i32, String> {
parse_int(s)
.and_then(check_positive)
.and_then(check_even)
}
Rust's ? Operator
Rust:
fn validate_input(s: &str) -> Result<i32, String> {
let n = parse_int(s)?; // early return on Err
let n = check_positive(n)?; // early return on Err
check_even(n) // final result
}
Custom Error Types
OCaml:
type validation_error =
| ParseError of string
| NotPositive of int
| NotEven of int
Rust:
#[derive(Debug)]
enum ValidationError {
ParseError(String),
NotPositive(i32),
NotEven(i32),
}
// ? operator works with From trait for auto-conversion
Exercises
? for error propagation.Result::and_then from scratch using match and verify it matches the stdlib behavior.map_err to convert between different error types in a pipeline involving multiple library functions.and_then chains (not ?) and compare readability with the ? version.