🦀 Functional Rust
🎬 Error Handling in Rust Option, Result, the ? operator, and combinators.
📝 Text version (for readers / accessibility)

• Option represents a value that may or may not exist — Some(value) or None

• Result represents success (Ok) or failure (Err) — no exceptions needed

• The ? operator propagates errors up the call stack concisely

• Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations

• The compiler forces you to handle every error case — no silent failures

Example 072: Result Type — Railway-Oriented Error Handling

Difficulty: ⭐⭐ Category: Error Handling Concept: Chaining fallible operations using the Result type so errors short-circuit the pipeline automatically. OCaml defines a custom `>>=` bind operator; Rust provides both `and_then` and the `?` operator built into the language. OCaml → Rust insight: OCaml's `>>=` (bind) operator is Rust's `.and_then()` method, but Rust's `?` operator provides even more concise syntax for the same pattern — it's railway-oriented programming built into the language.
/// 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);
    }
}

fn main() {
    println!("{:?}", (r - 4.0).abs() < f64::EPSILON);
    println!("{:?}", (r - 5.0).abs() < f64::EPSILON);
    println!("{:?}", process("-4").is_err());
}
let ( >>= ) r f = match r with
  | Error e -> Error e
  | Ok v    -> f v

let ( >>| ) r f = match r with
  | Error e -> Error e
  | Ok v    -> Ok (f v)

let parse_int s =
  match int_of_string_opt s with
  | None   -> Error (Printf.sprintf "not an integer: %S" s)
  | Some n -> Ok n

let positive x =
  if x > 0 then Ok x
  else Error (Printf.sprintf "%d is not positive" x)

let sqrt_safe x =
  positive x >>| (fun n -> sqrt (float_of_int n))

let process s =
  parse_int s >>= positive >>= sqrt_safe

let () =
  assert (process "16" = Ok 4.0);
  assert (process "25" = Ok 5.0);
  assert (process "-4" = Error "-4 is not positive");
  assert (process "hello" = Error "not an integer: \"hello\"");
  assert (process "0" = Error "0 is not positive");
  print_endline "All assertions passed."

📊 Detailed 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 type`string` (polymorphic)`String` (or custom enum)
Parse int`int_of_string_opt``str::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)