๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
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-circuitNone propagates through `>>=` automaticallyNone/Err propagates through `and_then` / `?`
ReadabilityInfix operator reads naturally left-to-rightMethod chain also reads left-to-right
Multiple return typesPolymorphic, 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))