047 — Result Bind (and_then)
Tutorial Video
Text description (accessibility)
This video demonstrates the "047 — Result Bind (and_then)" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. `Result::and_then` (bind) sequences two fallible operations: apply a function that returns `Result` to the `Ok` value, propagating `Err` automatically. Key difference from OCaml: 1. **`?` vs `let*`**: Rust's `?` is built into the language. OCaml's `let*` requires the `ppx_let` preprocessor. The `?` operator is more ergonomic for long chains.
Tutorial
The Problem
Result::and_then (bind) sequences two fallible operations: apply a function that returns Result to the Ok value, propagating Err automatically. It is the monadic sequencing operation for Result — "do this, then do that, stopping at the first failure". This enables clean pipelines of fallible operations without nested match statements.
The classic example is a multi-step pipeline: validate input → parse → compute → format. Each step may fail; if any fails, the pipeline stops and propagates the error. Without and_then, this requires nested match blocks or repeated null-checking. With and_then, the happy path reads linearly.
🎯 Learning Outcomes
result.and_then(|v| fallible_transform(v)) to chain fallible operationsmap: and_then applies a function returning Resultand_then as the result monad's bind operationand_then to the ? operator: f()? desugars to an and_then chainResult::and_then(f) to sequence fallible operations — each step feeds its success value to the next? operator as shorthand for and_then-based error propagation in functions returning ResultCode Example
#![allow(clippy::all)]
// Result::and_then (bind): sequences fallible steps, short-circuiting on the first Err.
pub fn parse_int(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|_| format!("invalid integer: {}", s))
}
pub fn safe_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("division by zero".to_string())
} else {
Ok(a / b)
}
}
pub fn pipeline(s: &str, divisor: i32) -> Result<String, String> {
parse_int(s).and_then(|n| safe_div(n, divisor)).map(|n| n.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pipeline_ok() {
assert_eq!(pipeline("100", 5), Ok("20".to_string()));
}
#[test]
fn test_pipeline_parse_fails() {
assert_eq!(pipeline("abc", 5), Err("invalid integer: abc".to_string()));
}
#[test]
fn test_pipeline_div_fails() {
assert_eq!(pipeline("100", 0), Err("division by zero".to_string()));
}
}Key Differences
? vs let***: Rust's ? is built into the language. OCaml's let* requires the ppx_let preprocessor. The ? operator is more ergonomic for long chains.? operator can automatically convert error types via the From trait — ? on Result<T, IoError> in a function returning Result<T, AppError> calls AppError::from(e). OCaml requires explicit Result.map_error.and_then vs >>=**: Haskell uses >>= (pronounced "bind"). Rust calls it and_then. OCaml calls it bind. All are the same operation.and_then only threads through the success channel. Use or_else for the error channel: result.or_else(|e| fallback(e)).and_then sequences fallible steps:** Each step in the chain may fail. If any step returns Err, the chain short-circuits and propagates the error. Only Ok values proceed to the next step.Ok is the happy track; Err is the error track. and_then switches you to the error track on failure — you never switch back without explicit or_else.? operator shorthand:** let x = fallible_op()? is syntactic sugar for and_then in functions returning Result. The ? desugars to match fallible_op() { Ok(v) => v, Err(e) => return Err(e.into()) }.let*:** With ppx_let, let* x = fallible_op () in next_step x is the OCaml equivalent of fallible_op()?.and_then(|x| next_step(x)).OCaml Approach
OCaml's Result.bind r f: let bind r f = match r with Ok x -> f x | Error e -> Error e. Pipe style: parse_int s |> Result.bind safe_div |> Result.bind (fun n -> Ok (string_of_int n)). With let* (ppx_let): let* n = parse_int s in let* d = safe_div n 2 in Ok (string_of_int d) — this reads like imperative code with automatic error propagation.
Full Source
#![allow(clippy::all)]
// Result::and_then (bind): sequences fallible steps, short-circuiting on the first Err.
pub fn parse_int(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|_| format!("invalid integer: {}", s))
}
pub fn safe_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("division by zero".to_string())
} else {
Ok(a / b)
}
}
pub fn pipeline(s: &str, divisor: i32) -> Result<String, String> {
parse_int(s).and_then(|n| safe_div(n, divisor)).map(|n| n.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pipeline_ok() {
assert_eq!(pipeline("100", 5), Ok("20".to_string()));
}
#[test]
fn test_pipeline_parse_fails() {
assert_eq!(pipeline("abc", 5), Err("invalid integer: abc".to_string()));
}
#[test]
fn test_pipeline_div_fails() {
assert_eq!(pipeline("100", 0), Err("division by zero".to_string()));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pipeline_ok() {
assert_eq!(pipeline("100", 5), Ok("20".to_string()));
}
#[test]
fn test_pipeline_parse_fails() {
assert_eq!(pipeline("abc", 5), Err("invalid integer: abc".to_string()));
}
#[test]
fn test_pipeline_div_fails() {
assert_eq!(pipeline("100", 0), Err("division by zero".to_string()));
}
}
Deep Comparison
OCaml vs Rust: Result Bind
Overview
See the example.rs and example.ml files for detailed implementations.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Type system | Hindley-Milner | Ownership + traits |
| Memory | GC | Zero-cost abstractions |
| Mutability | Explicit ref | mut keyword |
| Error handling | Option/Result | Result<T, E> |
See README.md for detailed comparison.
Exercises
"host:port", splits on :, parses the port as u16, and validates the host is non-empty. Use and_then at each step.find_user(id).and_then(|u| find_posts(u.id)).and_then(|posts| render(posts)) with stub functions. Handle the case where each step might return Err("not found").and_then chain using (a) nested match, (b) and_then, (c) ? operator. Verify all three produce the same results. Discuss readability.pipeline(input: &str) -> Result<f64, String> that parses a string to integer, checks it's positive, then computes its square root — using three chained and_then calls.? operator**: Rewrite the same pipeline function using the ? operator instead of explicit and_then chains. Both must produce identical behavior.