ExamplesBy LevelBy TopicLearning Paths
857 Fundamental

FlatMap / Bind Chains

Functional Programming

Tutorial

The Problem

Long sequences of fallible or absent-value operations create deeply nested code with match or if let. FlatMap (bind/and_then) chains linearize these sequences: each step receives the value from the previous step and returns a new wrapped value. The chain short-circuits on the first failure. This enables multi-step data processing pipelines — parse JSON, extract a field, convert the type, look it up in a database, format the result — written as readable flat code without the pyramid of doom. FlatMap chains are the foundation of Rust async/await (which desugars to state machines over flatmapped futures), Rust's ? operator chains, and iterator flat_map.

🎯 Learning Outcomes

  • • Chain and_then calls to build multi-step Option/Result pipelines
  • • Understand early exit: the first None/Err terminates the chain immediately
  • • Use flat_map on iterators: map each element to an iterator and flatten the result
  • • Compare map + flatten vs. direct flat_map — they are equivalent
  • • Recognize the pattern in async Rust: future.await?.process()?.format()?
  • Code Example

    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())
    }

    Key Differences

    AspectRustOCaml
    Chain syntax.and_then(...) chain>>= or let*
    Mixed monadsOption::ok_or then Result chainOption.to_result ~none
    Iterator flatmapflat_map(f) on IteratorList.concat_map
    Early exitFirst None/Err terminatesSame
    Async equivalent?.await? chainslwt >>= chains
    ReadabilityMethod chainPipe or do-notation

    OCaml Approach

    OCaml chains with >>=: parse_json s >>= extract_field "key" >>= fun field -> process field. The transition from option to result uses Option.to_result ~none:"missing". OCaml's let* do-notation (ppx_let): let* json = parse_json s in let* field = extract_field "key" json in process field. This reads like sequential imperative code while retaining the monadic structure. List.concat_map (the List equivalent of flat_map) flattens nested lists.

    Full Source

    #![allow(clippy::all)]
    // 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))
    }
    
    #[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);
        }
    }
    ✓ Tests Rust test suite
    #[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);
        }
    }

    Deep Comparison

    Comparison: FlatMap/Bind Chains

    Long Chain with >>= vs and_then

    OCaml:

    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:

    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:

    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))
    

    Exercises

  • Implement a complete data pipeline: read a file, parse each line as JSON, extract a numeric field, compute the sum — using ? for error propagation.
  • Use Iterator::flat_map to expand a list of ranges into a flat list of integers.
  • Demonstrate that .map(f).flatten() is equivalent to .flat_map(f) with a concrete test.
  • Implement a then_with combinator that only executes the next step if the previous step succeeds, logging failures.
  • Write the same multi-step pipeline using explicit match statements and compare line count and readability.
  • Open Source Repos