943 Result Railway
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
Result<T, E> as a railway: Ok stays on the happy path, Err short-circuits to the error trackand_then — equivalent to OCaml's >>= on result? operator for ergonomic short-circuiting inside functions that return Result.map() without touching the error typeand_then(f) and let n = x?; f(n) are semantically equivalentCode 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
| Aspect | Rust | OCaml |
|---|---|---|
| Short-circuit syntax | ? operator — lightweight, reads like imperative code | >>= or let* — explicit monadic syntax |
| Combinator | and_then | Result.bind |
| Map success | .map(f) | Result.map f |
| Error type | Must be uniform or use Box<dyn Error>/anyhow | Polymorphic type variable 'e |
| Interoperability | ? converts via From trait | Manual 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);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Bind | Custom >>= operator | .and_then() method |
| Map | Custom >>| operator | .map() method |
| Early return | Not available | ? operator |
| Error propagation | Manual 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
? 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? 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 typesFurther Reading
Exercises
divide(x, y) step that returns Err when y == 0 and chain it into the pipeline.ProcessError { Parse, NotPositive, Overflow } and rewrite the pipeline with it.map_err to convert ProcessError into a human-readable String at the boundary.process_all(inputs: &[&str]) -> Vec<Result<f64, String>> and then use partition to separate successes from failures.and_then chain vs ? operator in terms of generated code using cargo expand or by reading the desugared output.