โข Option
โข Result
โข 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
โข Option
โข Result
โข 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
// Without and_then: nesting grows with each step
fn validate_input(s: &str) -> Result<i32, String> {
match s.parse::<i32>() {
Err(_) => Err(format!("Not an integer: {}", s)),
Ok(n) => {
if n <= 0 {
Err(format!("Not positive: {}", n))
} else {
match n % 2 {
1 => Err(format!("Not even: {}", n)),
_ => Ok(n),
}
}
}
}
}
Every step adds another level of indentation. The happy path runs down the rightmost column. Error handling dominates the structure. If you add a fourth validation step, you nest again.
The real pain: `Err` propagates identically through every step โ if any step fails, the whole chain stops and carries that error forward. You're writing the same pattern repeatedly to express a simple idea: "do this, then this, then this, and if anything fails, stop and tell me why."
The Result monad exists to solve exactly that pain.
// Three inspectors, connected
parse_int(s) // Station 1: is it a number?
.and_then(check_positive) // Station 2: is it positive?
.and_then(check_even) // Station 3: is it even?
If station 1 fails with `Err("Not an integer: foo")`, stations 2 and 3 never run. The error arrives at the end exactly as produced. This is a monad: a pattern for chaining operations that carry failure context, without nesting. And again โ `?` is the same thing written as early return.
fn parse_int(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|_| format!("Not an integer: {}", s))
// map_err converts parse's error type into our String error
}
fn check_positive(n: i32) -> Result<i32, String> {
if n > 0 { Ok(n) } else { Err(format!("Not positive: {}", n)) }
}
fn check_even(n: i32) -> Result<i32, String> {
if n % 2 == 0 { Ok(n) } else { Err(format!("Not even: {}", n)) }
}
Chain them with `and_then`
fn validate_input(s: &str) -> Result<i32, String> {
parse_int(s)
.and_then(check_positive) // only runs if parse_int returned Ok
.and_then(check_even) // only runs if check_positive returned Ok
}
validate_input("42") // Ok(42)
validate_input("hello") // Err("Not an integer: hello")
validate_input("-4") // Err("Not positive: -4")
validate_input("7") // Err("Not even: 7")
The same chain with `?`
fn validate_input(s: &str) -> Result<i32, String> {
let n = parse_int(s)?; // returns Err early if this fails
let n = check_positive(n)?; // returns Err early if this fails
let n = check_even(n)?; // returns Err early if this fails
Ok(n)
}
Identical behavior, different style. `?` is cleaner when steps have names; `.and_then()` is cleaner for one-liners or lambdas.
Typed errors (the idiomatic Rust upgrade)
When you want the caller to distinguish error cases at compile time, use an enum instead of `String`:
#[derive(Debug, PartialEq)]
enum ValidationError {
ParseError(String),
NotPositive(i32),
NotEven(i32),
}
fn validate_typed(s: &str) -> Result<i32, ValidationError> {
let n = parse_int_typed(s)?; // Err(ValidationError::ParseError(...))
let n = check_positive_typed(n)?; // Err(ValidationError::NotPositive(...))
check_even_typed(n) // Err(ValidationError::NotEven(...))
}
Now the caller can `match` on the exact variant โ no string parsing needed to figure out what went wrong.
| Concept | OCaml | Rust |
|---|---|---|
| Monadic bind | `Result.bind` / `>>=` operator | `Result::and_then` |
| Do-notation sugar | Not built in | `?` operator |
| Error types | Strings or polymorphic variants common | Custom error enums idiomatic |
| Error conversion | Explicit mapping required | `From` trait + `?` auto-converts |
| Short-circuit | `Err` propagates through bind | `Err` propagates through `and_then` / `?` |
// Example 056: Result Monad
// Result monad: chain computations that may fail with error info
// Approach 1: and_then chains
fn parse_int(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|_| format!("Not an integer: {}", s))
}
fn check_positive(n: i32) -> Result<i32, String> {
if n > 0 { Ok(n) } else { Err(format!("Not positive: {}", n)) }
}
fn check_even(n: i32) -> Result<i32, String> {
if n % 2 == 0 { Ok(n) } else { Err(format!("Not even: {}", n)) }
}
fn validate_input(s: &str) -> Result<i32, String> {
parse_int(s)
.and_then(check_positive)
.and_then(check_even)
}
// Approach 2: Using ? operator (Rust's monadic do-notation)
fn validate_input_question(s: &str) -> Result<i32, String> {
let n = parse_int(s)?;
let n = check_positive(n)?;
let n = check_even(n)?;
Ok(n)
}
// Approach 3: Map and bind combined
fn double_validated(s: &str) -> Result<i32, String> {
validate_input(s).map(|n| n * 2)
}
// Bonus: custom error type with From for automatic ? conversion
#[derive(Debug, PartialEq)]
enum ValidationError {
ParseError(String),
NotPositive(i32),
NotEven(i32),
}
fn parse_int_typed(s: &str) -> Result<i32, ValidationError> {
s.parse::<i32>().map_err(|_| ValidationError::ParseError(s.to_string()))
}
fn check_positive_typed(n: i32) -> Result<i32, ValidationError> {
if n > 0 { Ok(n) } else { Err(ValidationError::NotPositive(n)) }
}
fn check_even_typed(n: i32) -> Result<i32, ValidationError> {
if n % 2 == 0 { Ok(n) } else { Err(ValidationError::NotEven(n)) }
}
fn validate_typed(s: &str) -> Result<i32, ValidationError> {
let n = parse_int_typed(s)?;
let n = check_positive_typed(n)?;
check_even_typed(n)
}
fn main() {
println!("validate '42': {:?}", validate_input("42"));
println!("validate 'hello': {:?}", validate_input("hello"));
println!("validate '-4': {:?}", validate_input("-4"));
println!("validate '7': {:?}", validate_input("7"));
println!("double '42': {:?}", double_validated("42"));
println!("typed '42': {:?}", validate_typed("42"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_input() {
assert_eq!(validate_input("42"), Ok(42));
}
#[test]
fn test_parse_error() {
assert_eq!(validate_input("hello"), Err("Not an integer: hello".into()));
}
#[test]
fn test_not_positive() {
assert_eq!(validate_input("-4"), Err("Not positive: -4".into()));
}
#[test]
fn test_not_even() {
assert_eq!(validate_input("7"), Err("Not even: 7".into()));
}
#[test]
fn test_question_mark_same_as_and_then() {
for s in &["42", "hello", "-4", "7"] {
assert_eq!(validate_input(s), validate_input_question(s));
}
}
#[test]
fn test_double() {
assert_eq!(double_validated("42"), Ok(84));
}
#[test]
fn test_typed_errors() {
assert_eq!(validate_typed("42"), Ok(42));
assert_eq!(validate_typed("bad"), Err(ValidationError::ParseError("bad".into())));
assert_eq!(validate_typed("-2"), Err(ValidationError::NotPositive(-2)));
assert_eq!(validate_typed("3"), Err(ValidationError::NotEven(3)));
}
}
(* Example 056: Result Monad *)
(* Result monad: chain computations that may fail with error info *)
let bind r f = match r with Error e -> Error e | Ok x -> f x
let ( >>= ) = bind
let return_ x = Ok x
(* Approach 1: Parsing pipeline *)
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 check_positive n =
if n > 0 then Ok n
else Error (Printf.sprintf "Not positive: %d" n)
let check_even n =
if n mod 2 = 0 then Ok n
else Error (Printf.sprintf "Not even: %d" n)
let validate_input s =
parse_int s >>= check_positive >>= check_even
(* Approach 2: Using Result.bind from stdlib *)
let validate_input_stdlib s =
Result.bind (parse_int s) (fun n ->
Result.bind (check_positive n) (fun n ->
check_even n))
(* Approach 3: Map and bind combined *)
let double_validated s =
validate_input s |> Result.map (fun n -> n * 2)
let () =
assert (validate_input "42" = Ok 42);
assert (validate_input "hello" = Error "Not an integer: hello");
assert (validate_input "-4" = Error "Not positive: -4");
assert (validate_input "7" = Error "Not even: 7");
assert (double_validated "42" = Ok 84);
(* Stdlib version *)
assert (validate_input_stdlib "42" = Ok 42);
assert (validate_input_stdlib "bad" = Error "Not an integer: bad");
Printf.printf "โ All tests passed\n"
OCaml:
๐ช Show OCaml equivalent
let validate_input s =
parse_int s >>= check_positive >>= check_even
Rust:
fn validate_input(s: &str) -> Result<i32, String> {
parse_int(s)
.and_then(check_positive)
.and_then(check_even)
}Rust:
fn validate_input(s: &str) -> Result<i32, String> {
let n = parse_int(s)?; // early return on Err
let n = check_positive(n)?; // early return on Err
check_even(n) // final result
}OCaml:
๐ช Show OCaml equivalent
type validation_error =
| ParseError of string
| NotPositive of int
| NotEven of int
Rust:
#[derive(Debug)]
enum ValidationError {
ParseError(String),
NotPositive(i32),
NotEven(i32),
}
// ? operator works with From trait for auto-conversion