ExamplesBy LevelBy TopicLearning Paths
056 Expert

056 — Result as a Monad

Functional Programming

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

  • • Recognize Result as a monad with Ok as return and and_then as bind
  • • Build multi-step fallible pipelines using and_then (monadic bind)
  • • Compare and_then chaining with the ? operator — both express the same computation
  • • Understand why Result is a monad but Validation (example 054) is not
  • • Use map (functor) and and_then (monad) as the complete Result transformation toolkit
  • • Sequence fallible operations with Result::and_then — the monadic bind that propagates errors automatically
  • • Verify monad laws: left identity Ok(x).and_then(f) == f(x), right identity r.and_then(Ok) == r
  • Code 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.
  • Monad laws: Left identity: 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.
  • Error type consistency: Monadic 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.
  • Sequencing fallible operations: The power of the 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.
  • **OCaml's 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());
        }
    }
    ✓ Tests Rust test suite
    #[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 functions
  • Result.map result f — transforms the Ok value
  • • Manual pattern matching as alternative
  • let* syntax with binding operators (OCaml 4.08+)
  • Rust Approach

  • .and_then(f) — monadic bind
  • .map(f) — functor map
  • ? operator — desugar to match + early return
  • • Method chaining is idiomatic
  • Comparison Table

    OperationOCamlRust
    Return/wrapOk xOk(x)
    BindResult.bind r fr.and_then(f)
    MapResult.map f rr.map(f)
    Sugarlet* x = r in ...let x = r?;
    Short-circuitPattern match? operator

    Exercises

  • Monad laws test: Write tests verifying the three monad laws for Result<i32, String>. For each law, construct a concrete case and assert equality.
  • State monad: Implement a 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.
  • Continuation monad: Implement 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 composition: Implement 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.
  • MonadPlus: Implement 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.
  • Open Source Repos