058: FlatMap/Bind Chains
Difficulty: 2 Level: Intermediate Build long chains of fallible operations where the first `None` or `Err` short-circuits everything.The Problem This Solves
Some operations are inherently sequential and dependent: to find a user's manager, you first look up the user, then their department, then the manager in that department. Each step can fail (not found). You can't do step 3 without the result of step 2. In imperative code, this becomes nested conditionals: `if user: if dept: if manager:`. Three levels deep for three lookups, five levels for five. The actual logic โ what you're computing โ gets lost in the nesting. It's the classic "pyramid of doom." `and_then` (also known as `bind` or `flatMap`) is the functional solution: each step in the chain receives the previous step's value and decides whether to continue. The first `None` or `Err` short-circuits the entire chain. No nesting, no intermediate `if` checks, no explicit early returns.The Intuition
You've seen this pattern with JavaScript Promises: `fetch(url).then(parse).then(validate).catch(handle)`. Each `.then()` receives the previous resolved value; if any step rejects, it skips to `.catch()`. `and_then` is exactly this, but for `Option`/`Result`, synchronously. The mental model: `and_then` threads a value through a pipeline of functions that can each say "I give up" by returning `None` or `Err`. If any step gives up, the entire computation gives up. You only need to handle failure once, at the end. `and_then` vs `map`: if the next function can fail, use `and_then`. If it can't fail, use `map`. The difference is in the return type of the function you pass.How It Works in Rust
// Database-like lookup chain with ? operator
fn find_manager_dept_name(user_id: u32, users: &[User], depts: &[Dept]) -> Option<String> {
let user = users.iter().find(|u| u.id == user_id)?; // None if not found
let dept = depts.iter().find(|d| d.id == user.dept_id)?; // None if not found
let manager = users.iter().find(|u| u.id == dept.mgr_id)?; // None if not found
Some(format!("{}'s manager is {} in {}", user.name, manager.name, dept.name))
}
// Equivalent with and_then chain (no ? operator)
fn find_manager_chain(user_id: u32, users: &[User], depts: &[Dept]) -> Option<String> {
users.iter().find(|u| u.id == user_id)
.and_then(|user| depts.iter().find(|d| d.id == user.dept_id)
.map(|dept| (user, dept)))
.and_then(|(user, dept)| users.iter().find(|u| u.id == dept.mgr_id)
.map(|mgr| format!("{}'s manager is {} in {}", user.name, mgr.name, dept.name)))
}
// Computation with bounds checking โ any step exceeding 100 stops the chain
fn compute() -> Option<i32> {
Some(0)
.and_then(|a| step_add(10, a)) // 0 + 10 = 10 โ
.and_then(|a| step_mul(3, a)) // 10 * 3 = 30 โ
.and_then(|a| step_add(20, a)) // 30 + 20 = 50 โ
.and_then(|a| step_add(40, a)) // 50 + 40 = 90 โ โ Some(90)
}
// One step exceeds limit โ whole computation returns None
let r = Some(50).and_then(|a| step_mul(3, a)); // 150 > 100 โ None
What This Unlocks
- Lookup chains: Multi-table database lookups, config file traversal, deeply nested JSON access โ all expressed as linear, readable chains.
- Data processing pipelines: parse โ extract field โ validate โ transform, each step explicit and each failure handled once at the end.
- Bounded accumulation: Sequences of steps with global invariants (e.g., "total must not exceed X") that short-circuit cleanly without global state.
Key Differences
| Concept | OCaml | Rust | ||||
|---|---|---|---|---|---|---|
| Bind operator | `>>=` (custom infix: `let (>>=) m f = ...`) | `.and_then(f)` (method on `Option`/`Result`) | ||||
| Style | `some_val >>= fun x -> next x >>= fun y -> ...` | `some_val.and_then(\ | x\ | next(x)).and_then(\ | y\ | ...)` |
| ? equivalent | `let* x = expr in ...` (OCaml 4.08+ binding operators) | `let x = expr?;` | ||||
| Short-circuit | None propagates through `>>=` automatically | None/Err propagates through `and_then` / `?` | ||||
| Readability | Infix operator reads naturally left-to-right | Method chain also reads left-to-right | ||||
| Multiple return types | Polymorphic, any monad | `Option` and `Result` have separate `and_then` |
// Example 058: FlatMap/Bind Chains
// Long monadic chains for sequential computation with early exit
// Approach 1: Multi-step data processing pipeline
fn parse_json(s: &str) -> Option<&str> {
if s.starts_with('{') { Some(s) } else { None }
}
fn extract_field<'a>(key: &str, json: &'a str) -> Option<&'a str> {
let pattern = format!("\"{}\":\"", key);
let start = json.find(&pattern)? + pattern.len();
let rest = &json[start..];
let end = rest.find('"')?;
Some(&rest[..end])
}
fn validate_length(min: usize, max: usize, s: &str) -> Option<&str> {
if s.len() >= min && s.len() <= max { Some(s) } else { None }
}
fn process_name(json: &str) -> Option<String> {
parse_json(json)
.and_then(|j| extract_field("name", j))
.and_then(|name| validate_length(1, 50, name))
.map(|s| s.to_uppercase())
}
// Approach 2: Database-like lookup chain with ?
#[derive(Clone, Debug)]
struct User { id: u32, dept_id: u32, name: String }
#[derive(Clone, Debug)]
struct Dept { id: u32, mgr_id: u32, name: String }
fn find_manager_dept_name(
user_id: u32,
users: &[User],
depts: &[Dept],
) -> Option<String> {
let user = users.iter().find(|u| u.id == user_id)?;
let dept = depts.iter().find(|d| d.id == user.dept_id)?;
let manager = users.iter().find(|u| u.id == dept.mgr_id)?;
Some(format!("{}'s manager is {} in {}", user.name, manager.name, dept.name))
}
// Approach 3: Computation with bounds checking
fn step_add(n: i32, acc: i32) -> Option<i32> {
let result = acc + n;
if result > 100 { None } else { Some(result) }
}
fn step_mul(n: i32, acc: i32) -> Option<i32> {
let result = acc * n;
if result > 100 { None } else { Some(result) }
}
fn compute() -> Option<i32> {
Some(0)
.and_then(|a| step_add(10, a))
.and_then(|a| step_mul(3, a))
.and_then(|a| step_add(20, a))
.and_then(|a| step_add(40, a))
}
fn main() {
println!("process_name: {:?}", process_name("{\"name\":\"alice\"}"));
println!("process_name bad: {:?}", process_name("not json"));
let users = vec![
User { id: 1, dept_id: 10, name: "Alice".into() },
User { id: 2, dept_id: 20, name: "Bob".into() },
];
let depts = vec![
Dept { id: 10, mgr_id: 2, name: "Engineering".into() },
Dept { id: 20, mgr_id: 1, name: "Marketing".into() },
];
println!("Manager: {:?}", find_manager_dept_name(1, &users, &depts));
println!("Compute: {:?}", compute());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_name_valid() {
assert_eq!(process_name("{\"name\":\"alice\"}"), Some("ALICE".to_string()));
}
#[test]
fn test_process_name_invalid_json() {
assert_eq!(process_name("not json"), None);
}
#[test]
fn test_process_name_missing_field() {
assert_eq!(process_name("{\"age\":\"30\"}"), None);
}
fn setup() -> (Vec<User>, Vec<Dept>) {
let users = vec![
User { id: 1, dept_id: 10, name: "Alice".into() },
User { id: 2, dept_id: 20, name: "Bob".into() },
];
let depts = vec![
Dept { id: 10, mgr_id: 2, name: "Engineering".into() },
Dept { id: 20, mgr_id: 1, name: "Marketing".into() },
];
(users, depts)
}
#[test]
fn test_find_manager() {
let (users, depts) = setup();
let result = find_manager_dept_name(1, &users, &depts);
assert_eq!(result, Some("Alice's manager is Bob in Engineering".to_string()));
}
#[test]
fn test_find_manager_missing() {
let (users, depts) = setup();
assert_eq!(find_manager_dept_name(99, &users, &depts), None);
}
#[test]
fn test_compute() {
assert_eq!(compute(), Some(90)); // 0+10=10, 10*3=30, 30+20=50, 50+40=90
}
#[test]
fn test_compute_overflow() {
// If we added step that would exceed 100
let r = Some(50)
.and_then(|a| step_mul(3, a)); // 150 > 100
assert_eq!(r, None);
}
}
(* Example 058: FlatMap/Bind Chains *)
(* Long monadic chains for sequential computation with early exit *)
let ( >>= ) m f = match m with None -> None | Some x -> f x
let return_ x = Some x
(* Approach 1: Multi-step data processing pipeline *)
let parse_json s =
if String.length s > 0 && s.[0] = '{' then Some s else None
let extract_field key json =
(* simplified: look for "key":"value" *)
let pattern = Printf.sprintf "\"%s\":\"" key in
match String.split_on_char '"' json with
| _ when not (String.contains json ':') -> None
| parts ->
let rec find = function
| k :: _ :: v :: _ when k = key -> Some v
| _ :: rest -> find rest
| [] -> None
in find parts
let validate_length min max s =
let len = String.length s in
if len >= min && len <= max then Some s else None
let to_uppercase s = Some (String.uppercase_ascii s)
let process_name json =
parse_json json >>= fun j ->
extract_field "name" j >>= fun name ->
validate_length 1 50 name >>= fun valid ->
to_uppercase valid
(* Approach 2: Database-like lookup chain *)
type user = { id: int; dept_id: int; name: string }
type dept = { d_id: int; mgr_id: int; d_name: string }
let users = [
{ id = 1; dept_id = 10; name = "Alice" };
{ id = 2; dept_id = 20; name = "Bob" }
]
let depts = [
{ d_id = 10; mgr_id = 2; d_name = "Engineering" };
{ d_id = 20; mgr_id = 1; d_name = "Marketing" }
]
let find_user id = List.find_opt (fun u -> u.id = id) users
let find_dept id = List.find_opt (fun d -> d.d_id = id) depts
let find_manager_dept_name user_id =
find_user user_id >>= fun user ->
find_dept user.dept_id >>= fun dept ->
find_user dept.mgr_id >>= fun manager ->
Some (Printf.sprintf "%s's manager is %s in %s" user.name manager.name dept.d_name)
(* Approach 3: Computation with accumulator *)
let step_add n acc = if acc + n > 100 then None else Some (acc + n)
let step_mul n acc = if acc * n > 100 then None else Some (acc * n)
let compute () =
return_ 0 >>=
step_add 10 >>=
step_mul 3 >>=
step_add 20 >>=
step_add 40
let () =
assert (process_name "{\"name\":\"alice\"}" = Some "ALICE");
assert (process_name "not json" = None);
assert (find_manager_dept_name 1 = Some "Alice's manager is Bob in Engineering");
assert (find_manager_dept_name 99 = None);
assert (compute () = Some 90);
Printf.printf "โ All tests passed\n"
๐ Detailed Comparison
Comparison: FlatMap/Bind Chains
Long Chain with >>= vs and_then
OCaml:
๐ช Show OCaml equivalent
let process_name json =
parse_json json >>= fun j ->
extract_field "name" j >>= fun name ->
validate_length 1 50 name >>= fun valid ->
to_uppercase valid
Rust (and_then):
fn process_name(json: &str) -> Option<String> {
parse_json(json)
.and_then(|j| extract_field("name", j))
.and_then(|name| validate_length(1, 50, name))
.map(|s| s.to_uppercase())
}Database Lookup Chain
OCaml:
๐ช Show OCaml equivalent
find_user user_id >>= fun user ->
find_dept user.dept_id >>= fun dept ->
find_user dept.mgr_id >>= fun manager ->
Some (user.name ^ "'s manager is " ^ manager.name)
Rust (? operator):
fn find_manager(user_id: u32, users: &[User], depts: &[Dept]) -> Option<String> {
let user = users.iter().find(|u| u.id == user_id)?;
let dept = depts.iter().find(|d| d.id == user.dept_id)?;
let mgr = users.iter().find(|u| u.id == dept.mgr_id)?;
Some(format!("{}'s manager is {}", user.name, mgr.name))
}Bounded Computation
OCaml:
๐ช Show OCaml equivalent
return_ 0 >>= step_add 10 >>= step_mul 3 >>= step_add 20
Rust:
Some(0)
.and_then(|a| step_add(10, a))
.and_then(|a| step_mul(3, a))
.and_then(|a| step_add(20, a))