072 — Railway-Oriented Programming
Tutorial
The Problem
Railway-oriented programming (ROP), coined by Scott Wlaschin in his 2014 talk and blog series, is a visual metaphor for monadic error handling. Picture a two-track railway: the "happy track" carries Ok values forward; the "error track" carries Err values that bypass all remaining steps. Each processing function is a switch: if the input arrives on the happy track and the function succeeds, the output stays on the happy track; if the function fails, the output switches to the error track and stays there for all subsequent steps.
This pattern, implemented via and_then chaining, produces code that reads top-to-bottom as a linear sequence of steps — exactly like procedural code with exceptions, but without hidden control flow. The ? operator in Rust is syntactic sugar for and_then applied to the current function's result type. Used in F#, Rust, Scala, Haskell, and any language where Result/Either is the primary error-handling mechanism, this pattern scales from simple validation pipelines to multi-step business logic with complex failure modes.
🎯 Learning Outcomes
and_then to form a railwayand_then step runs only on the happy trackvalidate_item, validate_quantity, validate_price as railway switchesand_then and ?Result monad behaviorCode Example
#![allow(clippy::all)]
// 072: Railway-Oriented Programming
// Chain Results — stay on happy path or switch to error track
#[derive(Debug, Clone)]
struct Order {
item: String,
quantity: i32,
price: f64,
}
// Individual validation steps
fn validate_quantity(order: Order) -> Result<Order, String> {
if order.quantity <= 0 {
Err("Quantity must be positive".into())
} else {
Ok(order)
}
}
fn validate_price(order: Order) -> Result<Order, String> {
if order.price <= 0.0 {
Err("Price must be positive".into())
} else {
Ok(order)
}
}
fn validate_item(order: Order) -> Result<Order, String> {
if order.item.is_empty() {
Err("Item name required".into())
} else {
Ok(order)
}
}
// Approach 1: and_then chain
fn validate_order_chain(order: Order) -> Result<Order, String> {
validate_item(order)
.and_then(validate_quantity)
.and_then(validate_price)
}
// Approach 2: Using ? operator
fn validate_order_question(order: Order) -> Result<Order, String> {
let o = validate_item(order)?;
let o = validate_quantity(o)?;
validate_price(o)
}
// Approach 3: Full pipeline
fn apply_discount(pct: f64, mut order: Order) -> Result<Order, String> {
if !(0.0..=100.0).contains(&pct) {
Err("Invalid discount".into())
} else {
order.price *= 1.0 - pct / 100.0;
Ok(order)
}
}
fn calculate_total(order: &Order) -> f64 {
order.quantity as f64 * order.price
}
fn process_order(order: Order, discount: f64) -> Result<f64, String> {
let o = validate_order_chain(order)?;
let o = apply_discount(discount, o)?;
Ok(calculate_total(&o))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_order() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert!(validate_order_chain(o).is_ok());
}
#[test]
fn test_invalid_quantity() {
let o = Order {
item: "Widget".into(),
quantity: -1,
price: 10.0,
};
assert_eq!(
validate_order_chain(o).unwrap_err(),
"Quantity must be positive"
);
}
#[test]
fn test_invalid_item() {
let o = Order {
item: "".into(),
quantity: 5,
price: 10.0,
};
assert_eq!(validate_order_chain(o).unwrap_err(), "Item name required");
}
#[test]
fn test_process_order() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert_eq!(process_order(o, 10.0), Ok(45.0));
}
#[test]
fn test_bad_discount() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert!(process_order(o, 200.0).is_err());
}
}Key Differences
and_then chains intuitive — you are either on the happy track or the error track. ? makes the track switch implicit.Validation pattern (example 072-error-accumulation). Knowing when to use which is key.>>= in OCaml**: Defining let (>>=) = Result.bind locally in OCaml makes railway chains very readable: a >>= f >>= g >>= h. Rust's method syntax a.and_then(f).and_then(g) is equivalent.OCaml Approach
OCaml's railway uses Result.bind or custom operators:
let validate_order order =
let ( >>= ) = Result.bind in
validate_item order
>>= validate_quantity
>>= validate_price
With OCaml 4.08+ binding operators, you can also write let* o = validate_item order in let* o = validate_quantity o in validate_price o. Each step receives the validated value from the previous step. The symmetry with Rust's .and_then chain is exact.
Full Source
#![allow(clippy::all)]
// 072: Railway-Oriented Programming
// Chain Results — stay on happy path or switch to error track
#[derive(Debug, Clone)]
struct Order {
item: String,
quantity: i32,
price: f64,
}
// Individual validation steps
fn validate_quantity(order: Order) -> Result<Order, String> {
if order.quantity <= 0 {
Err("Quantity must be positive".into())
} else {
Ok(order)
}
}
fn validate_price(order: Order) -> Result<Order, String> {
if order.price <= 0.0 {
Err("Price must be positive".into())
} else {
Ok(order)
}
}
fn validate_item(order: Order) -> Result<Order, String> {
if order.item.is_empty() {
Err("Item name required".into())
} else {
Ok(order)
}
}
// Approach 1: and_then chain
fn validate_order_chain(order: Order) -> Result<Order, String> {
validate_item(order)
.and_then(validate_quantity)
.and_then(validate_price)
}
// Approach 2: Using ? operator
fn validate_order_question(order: Order) -> Result<Order, String> {
let o = validate_item(order)?;
let o = validate_quantity(o)?;
validate_price(o)
}
// Approach 3: Full pipeline
fn apply_discount(pct: f64, mut order: Order) -> Result<Order, String> {
if !(0.0..=100.0).contains(&pct) {
Err("Invalid discount".into())
} else {
order.price *= 1.0 - pct / 100.0;
Ok(order)
}
}
fn calculate_total(order: &Order) -> f64 {
order.quantity as f64 * order.price
}
fn process_order(order: Order, discount: f64) -> Result<f64, String> {
let o = validate_order_chain(order)?;
let o = apply_discount(discount, o)?;
Ok(calculate_total(&o))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_order() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert!(validate_order_chain(o).is_ok());
}
#[test]
fn test_invalid_quantity() {
let o = Order {
item: "Widget".into(),
quantity: -1,
price: 10.0,
};
assert_eq!(
validate_order_chain(o).unwrap_err(),
"Quantity must be positive"
);
}
#[test]
fn test_invalid_item() {
let o = Order {
item: "".into(),
quantity: 5,
price: 10.0,
};
assert_eq!(validate_order_chain(o).unwrap_err(), "Item name required");
}
#[test]
fn test_process_order() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert_eq!(process_order(o, 10.0), Ok(45.0));
}
#[test]
fn test_bad_discount() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert!(process_order(o, 200.0).is_err());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_order() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert!(validate_order_chain(o).is_ok());
}
#[test]
fn test_invalid_quantity() {
let o = Order {
item: "Widget".into(),
quantity: -1,
price: 10.0,
};
assert_eq!(
validate_order_chain(o).unwrap_err(),
"Quantity must be positive"
);
}
#[test]
fn test_invalid_item() {
let o = Order {
item: "".into(),
quantity: 5,
price: 10.0,
};
assert_eq!(validate_order_chain(o).unwrap_err(), "Item name required");
}
#[test]
fn test_process_order() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert_eq!(process_order(o, 10.0), Ok(45.0));
}
#[test]
fn test_bad_discount() {
let o = Order {
item: "Widget".into(),
quantity: 5,
price: 10.0,
};
assert!(process_order(o, 200.0).is_err());
}
}
Deep Comparison
Core Insight
Railway-oriented programming models computation as two tracks: success and failure. Each function either continues on the success track or diverts to the error track. Result's bind/and_then is the switch.
OCaml Approach
Result.bind chains fallible operationslet* binding operators for monadic syntaxRust Approach
? operator is railway switching — returns Err early.and_then() for explicit chaining.map_err() to convert between error types on the error trackComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Switch to error | Error e | Err(e) / ? returns |
| Stay on success | Ok x | Ok(x) |
| Chain | Result.bind | .and_then() / ? |
| Transform error | Result.map_error | .map_err() |
Exercises
struct ValidationError { field: String, message: String }. Update the validators to include the field name. This enables structured error reporting.rollback function for a sequence of operations: if step 3 fails, undo steps 1 and 2. Model this with a stack of undo actions.