ExamplesBy LevelBy TopicLearning Paths
855 Expert GitHub

Result Monad

Functional Programming

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

  • • Understand and_then for Result: if Ok(x), apply f(x) returning Result<U, E>; if Err(e), return Err(e)
  • • Recognize ? as syntactic sugar: expr? = match expr { Ok(x) => x, Err(e) => return Err(e.into()) }
  • • Chain and_then calls to build pipelines that propagate errors without nested matches
  • • Distinguish map (transform Ok value, keep same error type) from and_then (can return new Err)
  • • Apply to: multi-step parsing pipelines, file I/O chains, network request sequences
  • Code Example

    fn validate_input(s: &str) -> Result<i32, String> {
        parse_int(s)
            .and_then(check_positive)
            .and_then(check_even)
    }

    Key Differences

    AspectRustOCaml
    BindResult::and_thenResult.bind
    Syntax sugar? operatorlet* (ppx_let or 4.08+)
    Error conversionmap_err / From traitResult.map_error
    Error propagation? calls .into() for type conversionManual Result.map_error
    Ok pathRight-biasRight-bias
    Multiple error typesBox<dyn Error> or anyhowstring 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)));
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Implement a multi-step file processing pipeline: read file → parse lines → validate each line → transform → write output, using ? for error propagation.
  • Implement Result::and_then from scratch using match and verify it matches the stdlib behavior.
  • Use map_err to convert between different error types in a pipeline involving multiple library functions.
  • Implement a pipeline using and_then chains (not ?) and compare readability with the ? version.
  • Demonstrate the short-circuit behavior: add logging to each step and show that steps after the first failure don't execute.
  • Open Source Repos