ExamplesBy LevelBy TopicLearning Paths
943 Intermediate

943 Result Railway

Functional Programming

Tutorial

The Problem

Implement railway-oriented programming with Rust's Result type. Chain fallible operations using and_then (monadic bind) and the ? operator so that the first error short-circuits the rest of the pipeline. Build a concrete pipeline: parse a string to integer, validate it is positive, then compute its square root. Compare the combinator-based approach with the ?-operator style.

🎯 Learning Outcomes

  • • Use Result<T, E> as a railway: Ok stays on the happy path, Err short-circuits to the error track
  • • Chain fallible operations with and_then — equivalent to OCaml's >>= on result
  • • Apply the ? operator for ergonomic short-circuiting inside functions that return Result
  • • Map over success values with .map() without touching the error type
  • • Understand that and_then(f) and let n = x?; f(n) are semantically equivalent
  • Code Example

    #![allow(clippy::all)]
    /// Result Type — Railway-Oriented Error Handling
    ///
    /// Using Result with combinators (and_then/map) for chaining fallible
    /// operations. Errors short-circuit the pipeline automatically.
    /// Rust's `?` operator makes this even more ergonomic than OCaml's `>>=`.
    
    /// Parse a string to i32.
    pub fn parse_int(s: &str) -> Result<i32, String> {
        s.parse::<i32>()
            .map_err(|_| format!("not an integer: {:?}", s))
    }
    
    /// Validate that a number is positive.
    pub fn positive(x: i32) -> Result<i32, String> {
        if x > 0 {
            Ok(x)
        } else {
            Err(format!("{} is not positive", x))
        }
    }
    
    /// Safe square root of a positive integer.
    pub fn sqrt_safe(x: i32) -> Result<f64, String> {
        positive(x).map(|n| (n as f64).sqrt())
    }
    
    /// Pipeline using `and_then` (equivalent to OCaml's `>>=` bind).
    pub fn process_bind(s: &str) -> Result<f64, String> {
        parse_int(s).and_then(positive).and_then(sqrt_safe)
    }
    
    /// Pipeline using the `?` operator — idiomatic Rust.
    pub fn process(s: &str) -> Result<f64, String> {
        let n = parse_int(s)?;
        let n = positive(n)?;
        let result = sqrt_safe(n)?;
        Ok(result)
    }
    
    /// Map over the Ok value without changing error type.
    pub fn process_doubled(s: &str) -> Result<f64, String> {
        process(s).map(|v| v * 2.0)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_valid_input() {
            let r = process("16").unwrap();
            assert!((r - 4.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn test_valid_25() {
            let r = process("25").unwrap();
            assert!((r - 5.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn test_negative() {
            assert!(process("-4").is_err());
            assert_eq!(process("-4").unwrap_err(), "-4 is not positive");
        }
    
        #[test]
        fn test_not_integer() {
            assert!(process("hello").is_err());
        }
    
        #[test]
        fn test_zero() {
            assert!(process("0").is_err());
        }
    
        #[test]
        fn test_bind_matches_question_mark() {
            for s in &["16", "25", "-4", "hello", "0"] {
                assert_eq!(process(s), process_bind(s));
            }
        }
    
        #[test]
        fn test_map() {
            let r = process_doubled("16").unwrap();
            assert!((r - 8.0).abs() < f64::EPSILON);
        }
    }

    Key Differences

    AspectRustOCaml
    Short-circuit syntax? operator — lightweight, reads like imperative code>>= or let* — explicit monadic syntax
    Combinatorand_thenResult.bind
    Map success.map(f)Result.map f
    Error typeMust be uniform or use Box<dyn Error>/anyhowPolymorphic type variable 'e
    Interoperability? converts via From traitManual adaptation needed

    The ? operator is Rust's answer to the verbosity of explicit match on every fallible call. It retains the type-safety of Result while reading almost as cleanly as exception-based code.

    OCaml Approach

    let parse_int s =
      match int_of_string_opt s with
      | Some n -> Ok n
      | None -> Error (Printf.sprintf "not an integer: %s" s)
    
    let positive x =
      if x > 0 then Ok x
      else Error (Printf.sprintf "%d is not positive" x)
    
    let sqrt_safe x =
      Result.bind (positive x) (fun n -> Ok (sqrt (float_of_int n)))
    
    (* Bind chain using >>= *)
    let ( >>= ) = Result.bind
    
    let process s =
      parse_int s >>= positive >>= sqrt_safe
    
    (* With let* (OCaml 4.08+, requires result.ml ppx or Result.bind) *)
    let process_letstar s =
      let* n = parse_int s in
      let* n = positive n in
      Ok (sqrt (float_of_int n))
    

    OCaml's Result.bind is the direct equivalent of Rust's and_then. The >>= operator alias makes pipelines read like Haskell do-notation. The let* syntax (monadic bind sugar) is the modern OCaml style.

    Full Source

    #![allow(clippy::all)]
    /// Result Type — Railway-Oriented Error Handling
    ///
    /// Using Result with combinators (and_then/map) for chaining fallible
    /// operations. Errors short-circuit the pipeline automatically.
    /// Rust's `?` operator makes this even more ergonomic than OCaml's `>>=`.
    
    /// Parse a string to i32.
    pub fn parse_int(s: &str) -> Result<i32, String> {
        s.parse::<i32>()
            .map_err(|_| format!("not an integer: {:?}", s))
    }
    
    /// Validate that a number is positive.
    pub fn positive(x: i32) -> Result<i32, String> {
        if x > 0 {
            Ok(x)
        } else {
            Err(format!("{} is not positive", x))
        }
    }
    
    /// Safe square root of a positive integer.
    pub fn sqrt_safe(x: i32) -> Result<f64, String> {
        positive(x).map(|n| (n as f64).sqrt())
    }
    
    /// Pipeline using `and_then` (equivalent to OCaml's `>>=` bind).
    pub fn process_bind(s: &str) -> Result<f64, String> {
        parse_int(s).and_then(positive).and_then(sqrt_safe)
    }
    
    /// Pipeline using the `?` operator — idiomatic Rust.
    pub fn process(s: &str) -> Result<f64, String> {
        let n = parse_int(s)?;
        let n = positive(n)?;
        let result = sqrt_safe(n)?;
        Ok(result)
    }
    
    /// Map over the Ok value without changing error type.
    pub fn process_doubled(s: &str) -> Result<f64, String> {
        process(s).map(|v| v * 2.0)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_valid_input() {
            let r = process("16").unwrap();
            assert!((r - 4.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn test_valid_25() {
            let r = process("25").unwrap();
            assert!((r - 5.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn test_negative() {
            assert!(process("-4").is_err());
            assert_eq!(process("-4").unwrap_err(), "-4 is not positive");
        }
    
        #[test]
        fn test_not_integer() {
            assert!(process("hello").is_err());
        }
    
        #[test]
        fn test_zero() {
            assert!(process("0").is_err());
        }
    
        #[test]
        fn test_bind_matches_question_mark() {
            for s in &["16", "25", "-4", "hello", "0"] {
                assert_eq!(process(s), process_bind(s));
            }
        }
    
        #[test]
        fn test_map() {
            let r = process_doubled("16").unwrap();
            assert!((r - 8.0).abs() < f64::EPSILON);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_valid_input() {
            let r = process("16").unwrap();
            assert!((r - 4.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn test_valid_25() {
            let r = process("25").unwrap();
            assert!((r - 5.0).abs() < f64::EPSILON);
        }
    
        #[test]
        fn test_negative() {
            assert!(process("-4").is_err());
            assert_eq!(process("-4").unwrap_err(), "-4 is not positive");
        }
    
        #[test]
        fn test_not_integer() {
            assert!(process("hello").is_err());
        }
    
        #[test]
        fn test_zero() {
            assert!(process("0").is_err());
        }
    
        #[test]
        fn test_bind_matches_question_mark() {
            for s in &["16", "25", "-4", "hello", "0"] {
                assert_eq!(process(s), process_bind(s));
            }
        }
    
        #[test]
        fn test_map() {
            let r = process_doubled("16").unwrap();
            assert!((r - 8.0).abs() < f64::EPSILON);
        }
    }

    Deep Comparison

    Result Type — Railway-Oriented Error Handling: OCaml vs Rust

    The Core Insight

    Both languages use Result types for composable error handling, but Rust elevates this pattern to a first-class language feature with the ? operator. OCaml requires defining custom bind operators; Rust builds them into the syntax. This makes "railway-oriented programming" — where errors automatically short-circuit a pipeline — natural in both languages.

    OCaml Approach

    OCaml defines custom operators: >>= (bind, aka and_then) and >>| (map). These compose fallible functions: parse_int s >>= positive >>= sqrt_safe. Each step either passes the Ok value forward or short-circuits on Error. This pattern must be manually implemented (though libraries like Base provide it). The operator definitions make the pipeline read left-to-right, mimicking monadic composition from Haskell.

    Rust Approach

    Rust's Result<T, E> has .and_then() and .map() as built-in methods, eliminating the need for custom operators. Even better, the ? operator desugars to an early return on Err, making imperative-style code as composable as the functional pipeline. Both styles (and_then chains and ? sequences) are idiomatic and produce identical results.

    Side-by-Side

    ConceptOCamlRust
    BindCustom >>= operator.and_then() method
    MapCustom >>| operator.map() method
    Early returnNot available? operator
    Error propagationManual via >>=Automatic via ?
    Error typestring (polymorphic)String (or custom enum)
    Parse intint_of_string_optstr::parse::<i32>()

    What Rust Learners Should Notice

  • • The ? operator is syntactic sugar for match result { Ok(v) => v, Err(e) => return Err(e.into()) } — it's railway-oriented programming built into the language
  • .and_then() is the functional style; ? is the imperative style — both are equally idiomatic in Rust
  • • Rust's ? also handles error type conversion via the From trait, enabling different error types in a single function
  • .map_err() transforms the error type without touching the success value — useful for unifying error types
  • • The monadic pattern (bind/map) is the same in both languages; Rust just provides more syntax sugar
  • Further Reading

  • • [The Rust Book — The ? Operator](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator)
  • • [Cornell CS3110 — Error Handling](https://cs3110.github.io/textbook/chapters/data/options.html)
  • Exercises

  • Add a divide(x, y) step that returns Err when y == 0 and chain it into the pipeline.
  • Define a custom error enum ProcessError { Parse, NotPositive, Overflow } and rewrite the pipeline with it.
  • Use map_err to convert ProcessError into a human-readable String at the boundary.
  • Implement process_all(inputs: &[&str]) -> Vec<Result<f64, String>> and then use partition to separate successes from failures.
  • Compare and_then chain vs ? operator in terms of generated code using cargo expand or by reading the desugared output.
  • Open Source Repos