Result Pattern Matching
Tutorial Video
Text description (accessibility)
This video demonstrates the "Result Pattern Matching" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Error handling is one of the most consequential design decisions in a language. Key difference from OCaml: 1. **`?` operator**: Rust's `?` is syntactic sugar for `match res { Ok(v) => v, Err(e) => return Err(e.into()) }`; OCaml uses `let*` or explicit bind (`>>=`).
Tutorial
The Problem
Error handling is one of the most consequential design decisions in a language. C uses integer return codes (frequently ignored). Java uses exceptions (can be swallowed silently). Rust uses Result<T, E> — a value that is either Ok(T) or Err(E), with the compiler enforcing that error paths are handled. The ? operator makes error propagation as concise as exceptions while keeping the error path explicit in function signatures. OCaml's result type serves the same purpose and is the direct ancestor of Rust's Result.
🎯 Learning Outcomes
match res { Ok(v) => ..., Err(e) => ... } handles both success and failure? propagates errors up the call stack in fn -> Result<_, E> functionsmap, and_then, map_err transform Result values without nested matchResult replaces exceptions: I/O, parsing, validation, network operationsCode Example
fn parse(s: &str) -> Result<i32, MyError> {
s.parse().map_err(|e| MyError::Parse(e.to_string()))
}
fn validate(n: i32) -> Result<i32, MyError> {
if (1..=100).contains(&n) { Ok(n) }
else { Err(MyError::Range(n)) }
}Key Differences
? operator**: Rust's ? is syntactic sugar for match res { Ok(v) => v, Err(e) => return Err(e.into()) }; OCaml uses let* or explicit bind (>>=).From trait**: Rust's ? automatically converts error types via From trait implementations; OCaml requires explicit map_err or pattern matching for type conversion.std::error::Error**: Rust has a standard Error trait for composable error types; OCaml uses ad-hoc exn or custom result types.anyhow/thiserror**: Rust's ecosystem has anyhow for application-level errors and thiserror for library errors; OCaml has no direct equivalent.OCaml Approach
OCaml's result type is the same concept:
type error = Parse of string | Range of int | DivZero
let (>>=) r f = match r with Ok x -> f x | Error e -> Error e
let parse_and_divide s n =
match int_of_string_opt s with
| None -> Error (Parse s)
| Some v -> if n = 0 then Error DivZero else Ok (v / n)
Full Source
#![allow(clippy::all)]
//! # Result Pattern Matching (Ok/Err)
//!
//! Handle fallible operations with the Result type and ? operator.
use std::num::ParseIntError;
/// Custom error type for demonstration.
#[derive(Debug, Clone, PartialEq)]
pub enum MyError {
Parse(String),
Range(i32),
DivZero,
}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MyError::Parse(s) => write!(f, "parse error: {}", s),
MyError::Range(n) => write!(f, "{} out of range", n),
MyError::DivZero => write!(f, "division by zero"),
}
}
}
impl std::error::Error for MyError {}
/// Parse a string to i32, mapping the error.
pub fn parse(s: &str) -> Result<i32, MyError> {
s.parse()
.map_err(|e: ParseIntError| MyError::Parse(e.to_string()))
}
/// Validate that a number is in range [1, 100].
pub fn validate(n: i32) -> Result<i32, MyError> {
if (1..=100).contains(&n) {
Ok(n)
} else {
Err(MyError::Range(n))
}
}
/// Safe division with error handling.
pub fn safe_div(a: i32, b: i32) -> Result<i32, MyError> {
if b == 0 {
Err(MyError::DivZero)
} else {
Ok(a / b)
}
}
/// Process using ? operator for early return on error.
pub fn process(s: &str) -> Result<i32, MyError> {
let n = parse(s)?;
let v = validate(n)?;
Ok(v * v)
}
/// Alternative using and_then combinators.
pub fn process_combinators(s: &str) -> Result<i32, MyError> {
parse(s).and_then(validate).map(|v| v * v)
}
/// Alternative using explicit match.
pub fn process_match(s: &str) -> Result<i32, MyError> {
match parse(s) {
Ok(n) => match validate(n) {
Ok(v) => Ok(v * v),
Err(e) => Err(e),
},
Err(e) => Err(e),
}
}
/// Convert Result to Option, discarding error info.
pub fn result_to_option<T, E>(r: Result<T, E>) -> Option<T> {
r.ok()
}
/// Convert Option to Result with custom error.
pub fn option_to_result<T>(opt: Option<T>, err: &str) -> Result<T, String> {
opt.ok_or_else(|| err.to_string())
}
/// Map error type.
pub fn map_error_example(s: &str) -> Result<i32, String> {
parse(s).map_err(|e| format!("Failed: {}", e))
}
/// Collect Vec<Result<T, E>> into Result<Vec<T>, E>.
pub fn collect_results(strings: &[&str]) -> Result<Vec<i32>, MyError> {
strings.iter().map(|s| parse(s)).collect()
}
/// Use unwrap_or_else for default on error.
pub fn parse_or_default(s: &str, default: i32) -> i32 {
parse(s).unwrap_or(default)
}
/// Chain multiple fallible operations.
pub fn complex_chain(a: &str, b: &str) -> Result<i32, MyError> {
let x = parse(a)?;
let y = parse(b)?;
let sum = x.checked_add(y).ok_or(MyError::Range(i32::MAX))?;
validate(sum)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid() {
assert_eq!(parse("42"), Ok(42));
assert_eq!(parse("-10"), Ok(-10));
}
#[test]
fn test_parse_invalid() {
assert!(parse("abc").is_err());
assert!(parse("").is_err());
}
#[test]
fn test_validate_in_range() {
assert_eq!(validate(1), Ok(1));
assert_eq!(validate(50), Ok(50));
assert_eq!(validate(100), Ok(100));
}
#[test]
fn test_validate_out_of_range() {
assert_eq!(validate(0), Err(MyError::Range(0)));
assert_eq!(validate(101), Err(MyError::Range(101)));
assert_eq!(validate(-5), Err(MyError::Range(-5)));
}
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10, 2), Ok(5));
assert_eq!(safe_div(10, 0), Err(MyError::DivZero));
}
#[test]
fn test_process_valid() {
assert_eq!(process("42"), Ok(1764)); // 42 * 42
assert_eq!(process("10"), Ok(100));
}
#[test]
fn test_process_parse_error() {
assert!(matches!(process("abc"), Err(MyError::Parse(_))));
}
#[test]
fn test_process_range_error() {
assert_eq!(process("0"), Err(MyError::Range(0)));
assert_eq!(process("101"), Err(MyError::Range(101)));
}
#[test]
fn test_process_approaches_equivalent() {
let cases = ["42", "abc", "0", "100", "101"];
for s in cases {
assert_eq!(process(s), process_combinators(s));
assert_eq!(process(s), process_match(s));
}
}
#[test]
fn test_result_to_option() {
assert_eq!(result_to_option(Ok::<_, ()>(42)), Some(42));
assert_eq!(result_to_option(Err::<i32, _>("error")), None);
}
#[test]
fn test_option_to_result() {
assert_eq!(option_to_result(Some(42), "missing"), Ok(42));
assert_eq!(
option_to_result(None::<i32>, "missing"),
Err("missing".to_string())
);
}
#[test]
fn test_collect_results_all_ok() {
let strings = vec!["1", "2", "3"];
assert_eq!(collect_results(&strings), Ok(vec![1, 2, 3]));
}
#[test]
fn test_collect_results_with_error() {
let strings = vec!["1", "x", "3"];
assert!(collect_results(&strings).is_err());
}
#[test]
fn test_parse_or_default() {
assert_eq!(parse_or_default("42", 0), 42);
assert_eq!(parse_or_default("abc", 0), 0);
}
#[test]
fn test_complex_chain() {
assert_eq!(complex_chain("10", "20"), Ok(30));
assert!(complex_chain("10", "abc").is_err());
assert!(complex_chain("50", "60").is_err()); // 110 out of range
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid() {
assert_eq!(parse("42"), Ok(42));
assert_eq!(parse("-10"), Ok(-10));
}
#[test]
fn test_parse_invalid() {
assert!(parse("abc").is_err());
assert!(parse("").is_err());
}
#[test]
fn test_validate_in_range() {
assert_eq!(validate(1), Ok(1));
assert_eq!(validate(50), Ok(50));
assert_eq!(validate(100), Ok(100));
}
#[test]
fn test_validate_out_of_range() {
assert_eq!(validate(0), Err(MyError::Range(0)));
assert_eq!(validate(101), Err(MyError::Range(101)));
assert_eq!(validate(-5), Err(MyError::Range(-5)));
}
#[test]
fn test_safe_div() {
assert_eq!(safe_div(10, 2), Ok(5));
assert_eq!(safe_div(10, 0), Err(MyError::DivZero));
}
#[test]
fn test_process_valid() {
assert_eq!(process("42"), Ok(1764)); // 42 * 42
assert_eq!(process("10"), Ok(100));
}
#[test]
fn test_process_parse_error() {
assert!(matches!(process("abc"), Err(MyError::Parse(_))));
}
#[test]
fn test_process_range_error() {
assert_eq!(process("0"), Err(MyError::Range(0)));
assert_eq!(process("101"), Err(MyError::Range(101)));
}
#[test]
fn test_process_approaches_equivalent() {
let cases = ["42", "abc", "0", "100", "101"];
for s in cases {
assert_eq!(process(s), process_combinators(s));
assert_eq!(process(s), process_match(s));
}
}
#[test]
fn test_result_to_option() {
assert_eq!(result_to_option(Ok::<_, ()>(42)), Some(42));
assert_eq!(result_to_option(Err::<i32, _>("error")), None);
}
#[test]
fn test_option_to_result() {
assert_eq!(option_to_result(Some(42), "missing"), Ok(42));
assert_eq!(
option_to_result(None::<i32>, "missing"),
Err("missing".to_string())
);
}
#[test]
fn test_collect_results_all_ok() {
let strings = vec!["1", "2", "3"];
assert_eq!(collect_results(&strings), Ok(vec![1, 2, 3]));
}
#[test]
fn test_collect_results_with_error() {
let strings = vec!["1", "x", "3"];
assert!(collect_results(&strings).is_err());
}
#[test]
fn test_parse_or_default() {
assert_eq!(parse_or_default("42", 0), 42);
assert_eq!(parse_or_default("abc", 0), 0);
}
#[test]
fn test_complex_chain() {
assert_eq!(complex_chain("10", "20"), Ok(30));
assert!(complex_chain("10", "abc").is_err());
assert!(complex_chain("50", "60").is_err()); // 110 out of range
}
}
Deep Comparison
OCaml vs Rust: Result Pattern Matching
Basic Result Functions
OCaml
let parse s = match int_of_string_opt s with
| Some n -> Ok n
| None -> Error (Printf.sprintf "not int: %s" s)
let validate n =
if n >= 1 && n <= 100 then Ok n
else Error (Printf.sprintf "%d out of range" n)
Rust
fn parse(s: &str) -> Result<i32, MyError> {
s.parse().map_err(|e| MyError::Parse(e.to_string()))
}
fn validate(n: i32) -> Result<i32, MyError> {
if (1..=100).contains(&n) { Ok(n) }
else { Err(MyError::Range(n)) }
}
Chaining with ? Operator
OCaml
let (let*) = Result.bind
let process s =
let* n = parse s in
let* v = validate n in
Ok (v * v)
Rust
fn process(s: &str) -> Result<i32, MyError> {
let n = parse(s)?;
let v = validate(n)?;
Ok(v * v)
}
Combinators Comparison
| Operation | OCaml | Rust |
|---|---|---|
| Map success | Result.map f r | r.map(f) |
| Map error | Result.map_error f r | r.map_err(f) |
| Chain | Result.bind r f | r.and_then(f) |
| Early return | let* binding | ? operator |
| To Option | Result.to_option r | r.ok() |
| From Option | Option.to_result ~none:e | opt.ok_or(e) |
| Unwrap or | Result.value r ~default:x | r.unwrap_or(x) |
Collecting Results
Rust
// Vec<Result<T, E>> -> Result<Vec<T>, E>
let results: Result<Vec<i32>, _> =
strings.iter().map(|s| parse(s)).collect();
OCaml
(* No built-in; use recursion or fold *)
let sequence_results =
List.fold_right (fun r acc ->
match r, acc with
| Ok x, Ok xs -> Ok (x :: xs)
| Error e, _ | _, Error e -> Error e
) results (Ok [])
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Error propagation | let* binding operator | ? operator |
| Type | ('a, 'e) result | Result<T, E> |
| Unwrap | Result.get_ok (may raise) | .unwrap() (panics) |
| Collect | Manual | Built-in collect() |
| Error trait | None | std::error::Error trait |
Exercises
fn parse_config(s: &str) -> Result<Config, MyError> that parses a "key=value" string using ? to propagate parse errors and a guard for invalid keys.impl From<ParseIntError> for MyError and verify that ? on a str::parse::<i32>() now automatically converts the error type.fn process(input: &str) -> Result<String, MyError> using only map, and_then, and map_err without any match or if let.