• 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
pub fn parse_int(s: &str) -> Result<i64, String> {
s.parse::<i64>().map_err(|_| format!("Not an integer: {s}"))
}
pub fn check_positive(n: i64) -> Result<i64, String> {
if n > 0 { Ok(n) } else { Err("Must be positive".to_string()) }
}
pub fn check_even(n: i64) -> Result<i64, String> {
if n % 2 == 0 { Ok(n) } else { Err("Must be even".to_string()) }
}
// Style 1: and_then chain — mirrors OCaml's >>= operator exactly
pub fn validate(s: &str) -> Result<i64, String> {
parse_int(s).and_then(check_positive).and_then(check_even)
}
// Style 2: ? operator — monadic sequencing with imperative style
pub fn validate_q(s: &str) -> Result<i64, String> {
let n = parse_int(s)?; // Err propagates immediately
let n = check_positive(n)?; // skipped if previous returned Err
let n = check_even(n)?;
Ok(n)
}
// Style 3: explicit bind — shows what and_then desugars to
fn bind<T, U, E>(r: Result<T, E>, f: impl FnOnce(T) -> Result<U, E>) -> Result<U, E> {
match r {
Err(e) => Err(e), // error track: skip f, propagate error
Ok(v) => f(v), // success track: apply f
}
}
`map_err` converts the `ParseIntError` from `str::parse` into our uniform `String` error type — a necessary step when combining functions with different error types.
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Bind operator | Custom `>>=` infix on `result` | `.and_then()` method — no custom operator | ||
| `?` equivalent | None | `?` desugars to early `return Err(e)` | ||
| Error conversion | String concatenation | `map_err(\ | _\ | ...)` converts error types |
| Error type | `string` for all steps | Generic `E` — same type across the chain | ||
| Syntax sugar | `let*` (bind notation, OCaml 4.08+) | `?` — idiomatic in all Rust code |
// Solution 1: Idiomatic Rust — Result's built-in monadic combinator
// `and_then` is Rust's bind (>>=) for Result: propagates Err, applies f to Ok
pub fn parse_int(s: &str) -> Result<i64, String> {
s.parse::<i64>().map_err(|_| format!("Not an integer: {s}"))
}
pub fn check_positive(n: i64) -> Result<i64, String> {
if n > 0 {
Ok(n)
} else {
Err("Must be positive".to_string())
}
}
pub fn check_even(n: i64) -> Result<i64, String> {
if n % 2 == 0 {
Ok(n)
} else {
Err("Must be even".to_string())
}
}
// Railway-oriented: each step either advances the train or diverts to the error track
pub fn validate_idiomatic(s: &str) -> Result<i64, String> {
parse_int(s).and_then(check_positive).and_then(check_even)
}
// Solution 2: Explicit bind — mirrors OCaml's >>= operator exactly
fn bind<T, U, E>(r: Result<T, E>, f: impl FnOnce(T) -> Result<U, E>) -> Result<U, E> {
match r {
Err(e) => Err(e),
Ok(x) => f(x),
}
}
pub fn validate_explicit(s: &str) -> Result<i64, String> {
bind(bind(parse_int(s), check_positive), check_even)
}
// Solution 3: Using the `?` operator — Rust's ergonomic monadic shorthand
pub fn validate_question_mark(s: &str) -> Result<i64, String> {
let n = parse_int(s)?;
let n = check_positive(n)?;
check_even(n)
}
fn main() {
let inputs = ["42", "-3", "abc", "7"];
println!("=== validate_idiomatic (and_then chain) ===");
for s in &inputs {
match validate_idiomatic(s) {
Ok(n) => println!("{s} -> Ok {n}"),
Err(e) => println!("{s} -> Error: {e}"),
}
}
println!("\n=== validate_question_mark (? operator) ===");
for s in &inputs {
match validate_question_mark(s) {
Ok(n) => println!("{s} -> Ok {n}"),
Err(e) => println!("{s} -> Error: {e}"),
}
}
println!("\n=== validate_explicit (explicit bind) ===");
for s in &inputs {
match validate_explicit(s) {
Ok(n) => println!("{s} -> Ok {n}"),
Err(e) => println!("{s} -> Error: {e}"),
}
}
}
/* Output:
=== validate_idiomatic (and_then chain) ===
42 -> Ok 42
-3 -> Error: Must be positive
abc -> Error: Not an integer: abc
7 -> Error: Must be even
=== validate_question_mark (? operator) ===
42 -> Ok 42
-3 -> Error: Must be positive
abc -> Error: Not an integer: abc
7 -> Error: Must be even
=== validate_explicit (explicit bind) ===
42 -> Ok 42
-3 -> Error: Must be positive
abc -> Error: Not an integer: abc
7 -> Error: Must be even
*/
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_positive_even_succeeds() {
assert_eq!(validate_idiomatic("42"), Ok(42));
assert_eq!(validate_explicit("42"), Ok(42));
assert_eq!(validate_question_mark("42"), Ok(42));
}
#[test]
fn test_negative_number_fails_check_positive() {
let expected = Err("Must be positive".to_string());
assert_eq!(validate_idiomatic("-3"), expected);
assert_eq!(validate_explicit("-3"), expected);
assert_eq!(validate_question_mark("-3"), expected);
}
#[test]
fn test_non_integer_string_fails_parse() {
let expected = Err("Not an integer: abc".to_string());
assert_eq!(validate_idiomatic("abc"), expected);
assert_eq!(validate_explicit("abc"), expected);
assert_eq!(validate_question_mark("abc"), expected);
}
#[test]
fn test_positive_odd_fails_check_even() {
let expected = Err("Must be even".to_string());
assert_eq!(validate_idiomatic("7"), expected);
assert_eq!(validate_explicit("7"), expected);
assert_eq!(validate_question_mark("7"), expected);
}
#[test]
fn test_zero_fails_check_positive() {
let expected = Err("Must be positive".to_string());
assert_eq!(validate_idiomatic("0"), expected);
assert_eq!(validate_explicit("0"), expected);
assert_eq!(validate_question_mark("0"), expected);
}
#[test]
fn test_parse_int_valid() {
assert_eq!(parse_int("100"), Ok(100));
assert_eq!(parse_int("-5"), Ok(-5));
assert_eq!(parse_int("0"), Ok(0));
}
#[test]
fn test_parse_int_invalid() {
assert!(parse_int("abc").is_err());
assert!(parse_int("1.5").is_err());
assert!(parse_int("").is_err());
}
#[test]
fn test_error_stops_at_first_failure() {
let result = validate_idiomatic("abc");
assert!(result.unwrap_err().contains("Not an integer"));
}
}
(* OCaml >>= for Result: short-circuit on Error, apply f to Ok *)
let ( >>= ) r f = match r with
| Error _ as e -> e
| Ok x -> f x
(* Idiomatic: parse string to int, returning Result *)
let parse_int s =
match int_of_string_opt s with
| Some n -> Ok n
| None -> Error ("Not an integer: " ^ s)
let check_positive n =
if n > 0 then Ok n else Error "Must be positive"
let check_even n =
if n mod 2 = 0 then Ok n else Error "Must be even"
(* Railway-oriented: chain validations with >>= *)
let validate s =
parse_int s >>= check_positive >>= check_even
(* Recursive-style: explicit function composition via bind *)
let rec bind r f = match r with
| Error _ as e -> e
| Ok x -> f x
let validate_explicit s =
bind (bind (parse_int s) check_positive) check_even
let () =
assert (validate "42" = Ok 42);
assert (validate "-3" = Error "Must be positive");
assert (validate "abc" = Error "Not an integer: abc");
assert (validate "7" = Error "Must be even");
assert (validate "0" = Error "Must be positive");
assert (validate_explicit "42" = Ok 42);
assert (validate_explicit "-3" = Error "Must be positive");
List.iter (fun s ->
match validate s with
| Ok n -> Printf.printf "%s -> Ok %d\n" s n
| Error e -> Printf.printf "%s -> Error: %s\n" s e
) ["42"; "-3"; "abc"; "7"];
print_endline "ok"
🐪 Show OCaml equivalent
let ( >>= ) r f = match r with
| Error _ as e -> e
| Ok x -> f x
let parse_int s =
match int_of_string_opt s with
| Some n -> Ok n
| None -> Error ("Not an integer: " ^ s)
let check_positive n =
if n > 0 then Ok n else Error "Must be positive"
let check_even n =
if n mod 2 = 0 then Ok n else Error "Must be even"
let validate s =
parse_int s >>= check_positive >>= check_even
pub fn parse_int(s: &str) -> Result<i64, String> {
s.parse::<i64>().map_err(|_| format!("Not an integer: {s}"))
}
pub fn check_positive(n: i64) -> Result<i64, String> {
if n > 0 { Ok(n) } else { Err("Must be positive".to_string()) }
}
pub fn check_even(n: i64) -> Result<i64, String> {
if n % 2 == 0 { Ok(n) } else { Err("Must be even".to_string()) }
}
pub fn validate_idiomatic(s: &str) -> Result<i64, String> {
parse_int(s).and_then(check_positive).and_then(check_even)
}pub fn validate_question_mark(s: &str) -> Result<i64, String> {
let n = parse_int(s)?;
let n = check_positive(n)?;
check_even(n)
}fn bind<T, U, E>(r: Result<T, E>, f: impl FnOnce(T) -> Result<U, E>) -> Result<U, E> {
match r {
Err(e) => Err(e),
Ok(x) => f(x),
}
}
pub fn validate_explicit(s: &str) -> Result<i64, String> {
bind(bind(parse_int(s), check_positive), check_even)
}| Concept | OCaml | Rust |
|---|---|---|
| Result type | `('a, 'b) result` | `Result<T, E>` |
| Bind operator | `val (>>=) : ('a,'e) result -> ('a -> ('b,'e) result) -> ('b,'e) result` | `fn and_then<U, F>(self, f: F) -> Result<U, E>` |
| Validate signature | `val validate : string -> (int, string) result` | `fn validate(s: &str) -> Result<i64, String>` |
| Error conversion | `"Not an integer: " ^ s` (string concat) | `format!("Not an integer: {s}")` + `map_err` |
| Short-circuit | `Error _ as e -> e` in `>>=` | Implicit in `and_then` / early return via `?` |
1. `and_then` is `>>=`: Rust's `Result::and_then` is the stdlib bind combinator — no custom operator needed. It unwraps `Ok` and passes the value to the next function, or passes `Err` through unchanged.
2. `?` is do-notation sugar: The `?` operator desugars to a match on `Result` that early-returns `Err`. Sequential `?` usage gives the same left-to-right chaining as `>>=` but looks like ordinary imperative code.
3. Error type uniformity: OCaml's polymorphic `result` lets you mix error types freely. Rust requires all steps in a chain to share the same `E` — here `String`. `map_err` is the idiomatic adapter when upstream errors differ.
4. First failure wins: In both languages the chain stops at the first `Err`/`Error`. `parse_int "abc"` never reaches `check_positive` or `check_even`; the error message reflects exactly which step failed.
5. Railway metaphor: Each validation function is a railway switch — the "success track" (`Ok`) continues forward; the "error track" (`Err`) bypasses all remaining steps and exits at the end. This pattern makes error handling compositional and centralized.
Use `and_then` chain when: The validators are already written as standalone functions and you want a point-free, functional pipeline that reads like a sentence.
Use `?` operator when: The validation logic is complex, needs intermediate let-bindings, or benefits from the familiar sequential look — particularly inside a function body with other logic.
Use explicit `bind` when: You are teaching the monad concept or need to abstract over multiple monadic types (e.g., building a generic combinator library).