ExamplesBy LevelBy TopicLearning Paths
855 Expert

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