ExamplesBy LevelBy TopicLearning Paths
1007 Intermediate

1007-result-combinators — Result Combinators

Functional Programming

Tutorial

The Problem

Chaining fallible operations without deeply nested pattern matches is a core challenge in error-handling design. In C, every call site checks a return code, creating pyramids of conditionals. Haskell's Either monad and OCaml's Result type both solve this by providing combinators that thread success values through a pipeline while propagating errors automatically.

Rust's Result<T, E> ships with a rich set of combinators: map, and_then, map_err, or_else, and unwrap_or_else. These methods let you compose fallible computations as cleanly as iterator chains, desugaring to the same match logic you would write by hand.

🎯 Learning Outcomes

  • • Understand how and_then implements monadic bind (flatmap) for Result
  • • Apply map to transform success values without unwrapping
  • • Use map_err to convert or annotate error types
  • • Chain or_else to substitute a fallback Result on failure
  • • Replace explicit match blocks with expressive combinator pipelines
  • Code Example

    #![allow(clippy::all)]
    // 1007: Result Combinators
    // and_then, or_else, map, map_err, unwrap_or_else
    
    fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>()
            .map_err(|e| format!("not an int: {} ({})", s, e))
    }
    
    fn double_if_positive(n: i64) -> Result<i64, String> {
        if n > 0 {
            Ok(n * 2)
        } else {
            Err("must be positive".into())
        }
    }
    
    // Approach 1: Chaining with and_then (flatmap/bind)
    fn process_chain(s: &str) -> Result<String, String> {
        parse_int(s)
            .and_then(double_if_positive)
            .map(|n| n.to_string())
    }
    
    // Approach 2: Using map, map_err, or_else, unwrap_or_else
    fn process_with_fallback(s: &str) -> String {
        parse_int(s)
            .and_then(double_if_positive)
            .map(|n| n.to_string())
            .map_err(|e| format!("FALLBACK: {}", e))
            .unwrap_or_else(|e| e)
    }
    
    fn process_or_else(s: &str) -> Result<i64, String> {
        parse_int(s).and_then(double_if_positive).or_else(|_| Ok(0)) // fallback to 0 on any error
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_and_then_success() {
            assert_eq!(process_chain("5"), Ok("10".to_string()));
        }
    
        #[test]
        fn test_and_then_negative() {
            assert_eq!(process_chain("-3"), Err("must be positive".to_string()));
        }
    
        #[test]
        fn test_and_then_parse_fail() {
            assert!(process_chain("abc").is_err());
        }
    
        #[test]
        fn test_map() {
            let result: Result<i64, String> = Ok(5);
            assert_eq!(result.map(|n| n * 2), Ok(10));
        }
    
        #[test]
        fn test_map_err() {
            let result: Result<i64, &str> = Err("low");
            assert_eq!(result.map_err(|e| e.to_uppercase()), Err("LOW".to_string()));
        }
    
        #[test]
        fn test_or_else() {
            assert_eq!(process_or_else("-1"), Ok(0));
            assert_eq!(process_or_else("5"), Ok(10));
        }
    
        #[test]
        fn test_unwrap_or_else() {
            let result: Result<i64, String> = Err("fail".into());
            assert_eq!(result.unwrap_or_else(|_| 99), 99);
    
            let result: Result<i64, String> = Ok(42);
            assert_eq!(result.unwrap_or_else(|_| 99), 42);
        }
    
        #[test]
        fn test_fallback_string() {
            assert_eq!(process_with_fallback("5"), "10");
            assert!(process_with_fallback("abc").starts_with("FALLBACK"));
        }
    }

    Key Differences

  • Method vs function syntax: Rust uses result.and_then(f) method chaining; OCaml uses Result.bind f result piped with |>.
  • Error type unification: Rust requires both branches of and_then to share the same E type; OCaml is structurally typed and more flexible.
  • **From conversion**: Rust's ? auto-converts error types via From; OCaml combinators require explicit Result.map_error for type adaptation.
  • Ownership: Rust combinators consume the Result value by move; OCaml passes values through the GC with no ownership concern.
  • OCaml Approach

    OCaml's Result module provides Result.map, Result.bind (equivalent to and_then), and Result.map_error. The |> pipeline operator makes chaining natural:

    let process s =
      parse_int s
      |> Result.bind double_if_positive
      |> Result.map string_of_int
    

    OCaml expresses combinators as regular functions passed via |>, while Rust uses method chaining on the Result value.

    Full Source

    #![allow(clippy::all)]
    // 1007: Result Combinators
    // and_then, or_else, map, map_err, unwrap_or_else
    
    fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>()
            .map_err(|e| format!("not an int: {} ({})", s, e))
    }
    
    fn double_if_positive(n: i64) -> Result<i64, String> {
        if n > 0 {
            Ok(n * 2)
        } else {
            Err("must be positive".into())
        }
    }
    
    // Approach 1: Chaining with and_then (flatmap/bind)
    fn process_chain(s: &str) -> Result<String, String> {
        parse_int(s)
            .and_then(double_if_positive)
            .map(|n| n.to_string())
    }
    
    // Approach 2: Using map, map_err, or_else, unwrap_or_else
    fn process_with_fallback(s: &str) -> String {
        parse_int(s)
            .and_then(double_if_positive)
            .map(|n| n.to_string())
            .map_err(|e| format!("FALLBACK: {}", e))
            .unwrap_or_else(|e| e)
    }
    
    fn process_or_else(s: &str) -> Result<i64, String> {
        parse_int(s).and_then(double_if_positive).or_else(|_| Ok(0)) // fallback to 0 on any error
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_and_then_success() {
            assert_eq!(process_chain("5"), Ok("10".to_string()));
        }
    
        #[test]
        fn test_and_then_negative() {
            assert_eq!(process_chain("-3"), Err("must be positive".to_string()));
        }
    
        #[test]
        fn test_and_then_parse_fail() {
            assert!(process_chain("abc").is_err());
        }
    
        #[test]
        fn test_map() {
            let result: Result<i64, String> = Ok(5);
            assert_eq!(result.map(|n| n * 2), Ok(10));
        }
    
        #[test]
        fn test_map_err() {
            let result: Result<i64, &str> = Err("low");
            assert_eq!(result.map_err(|e| e.to_uppercase()), Err("LOW".to_string()));
        }
    
        #[test]
        fn test_or_else() {
            assert_eq!(process_or_else("-1"), Ok(0));
            assert_eq!(process_or_else("5"), Ok(10));
        }
    
        #[test]
        fn test_unwrap_or_else() {
            let result: Result<i64, String> = Err("fail".into());
            assert_eq!(result.unwrap_or_else(|_| 99), 99);
    
            let result: Result<i64, String> = Ok(42);
            assert_eq!(result.unwrap_or_else(|_| 99), 42);
        }
    
        #[test]
        fn test_fallback_string() {
            assert_eq!(process_with_fallback("5"), "10");
            assert!(process_with_fallback("abc").starts_with("FALLBACK"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_and_then_success() {
            assert_eq!(process_chain("5"), Ok("10".to_string()));
        }
    
        #[test]
        fn test_and_then_negative() {
            assert_eq!(process_chain("-3"), Err("must be positive".to_string()));
        }
    
        #[test]
        fn test_and_then_parse_fail() {
            assert!(process_chain("abc").is_err());
        }
    
        #[test]
        fn test_map() {
            let result: Result<i64, String> = Ok(5);
            assert_eq!(result.map(|n| n * 2), Ok(10));
        }
    
        #[test]
        fn test_map_err() {
            let result: Result<i64, &str> = Err("low");
            assert_eq!(result.map_err(|e| e.to_uppercase()), Err("LOW".to_string()));
        }
    
        #[test]
        fn test_or_else() {
            assert_eq!(process_or_else("-1"), Ok(0));
            assert_eq!(process_or_else("5"), Ok(10));
        }
    
        #[test]
        fn test_unwrap_or_else() {
            let result: Result<i64, String> = Err("fail".into());
            assert_eq!(result.unwrap_or_else(|_| 99), 99);
    
            let result: Result<i64, String> = Ok(42);
            assert_eq!(result.unwrap_or_else(|_| 99), 42);
        }
    
        #[test]
        fn test_fallback_string() {
            assert_eq!(process_with_fallback("5"), "10");
            assert!(process_with_fallback("abc").starts_with("FALLBACK"));
        }
    }

    Deep Comparison

    Result Combinators — Comparison

    Core Insight

    Both OCaml and Rust treat Result as a monad with map (functor) and bind/and_then (monadic bind). Rust adds more built-in combinators.

    OCaml Approach

  • Result.map, Result.bind in stdlib (OCaml 4.08+)
  • • Custom map_error, or_else typically hand-written
  • • Pipeline via |> operator
  • Option.value ~default for unwrap-with-default
  • Rust Approach

  • • Rich built-in: map, map_err, and_then, or_else, unwrap_or_else, unwrap_or_default
  • • Method chaining with . notation
  • ? operator as syntactic sugar for and_then + early return
  • ok(), err() to convert between Result and Option
  • Comparison Table

    CombinatorOCamlRust
    mapResult.map f rr.map(f)
    flatmap/bindResult.bind r fr.and_then(f)
    map errorcustom map_errorr.map_err(f)
    fallbackcustom or_elser.or_else(f)
    defaultResult.value r ~defaultr.unwrap_or(v)
    lazy defaultcustomr.unwrap_or_else(f)

    Exercises

  • Add a clamp_to_range(n: i64, min: i64, max: i64) -> Result<i64, String> function and insert it into process_chain between parsing and doubling.
  • Rewrite process_with_fallback using the ? operator inside a helper function instead of combinator chaining. Verify all tests still pass.
  • Implement a map2 function that takes two Result<T, E> values and a combining function, returning Result<U, E>. Use it to add two parsed integers.
  • Open Source Repos