ExamplesBy LevelBy TopicLearning Paths
903 Intermediate

903-iterator-flat-map — Iterator flat_map

Functional Programming

Tutorial

The Problem

Many operations produce zero, one, or multiple outputs per input: tokenizing a sentence produces multiple words, parsing produces either a value or nothing, expanding a range produces many numbers. .flat_map(f) = .map(f).flatten() handles all three cases in one operation. In category theory, this is the monadic bind (>>=) — the mechanism that lets you chain computations where each step can produce zero or more results. OCaml has List.concat_map. Haskell has concatMap and (>>=) for lists. This is one of the most powerful iterator operations.

🎯 Learning Outcomes

  • • Use .flat_map(f) as the composition of map and flatten
  • • Produce zero outputs (silently drop) by mapping to empty iterators
  • • Produce multiple outputs by mapping to iterators with multiple elements
  • • Use .flat_map(|s| s.parse::<i32>()) to parse-and-filter in one pass
  • • Compare with OCaml's List.concat_map and Haskell's concatMap
  • Code Example

    // flat_map is lazy: no intermediate Vec allocated
    let words: Vec<&str> = ["hello world", "foo bar"]
        .iter()
        .flat_map(|s| s.split_whitespace())
        .collect();
    // → ["hello", "world", "foo", "bar"]
    
    let expanded: Vec<i32> = [1i32, 2, 3]
        .iter()
        .flat_map(|&n| 0..n)
        .collect();
    // → [0, 0, 1, 0, 1, 2]
    
    // Result/Option implements IntoIterator — failures yield zero elements
    let parsed: Vec<i32> = ["1", "two", "3"]
        .iter()
        .flat_map(|s| s.parse::<i32>().map(|n| n * 2))
        .collect();
    // → [2, 6]

    Key Differences

  • Result as iterator: Rust Result<T, E> implements IntoIterator (Ok yields T, Err yields nothing), enabling parse-and-filter with .flat_map(|s| s.parse()); OCaml uses filter_map with option.
  • Laziness: Rust .flat_map() is lazy; OCaml List.concat_map is eager.
  • Monadic identity: Both languages express the list monad's bind as flat_map; Rust has no trait for this abstraction, OCaml uses it via explicit function application.
  • Zero outputs: Both languages naturally drop zero-output cases — Rust via empty iterators, OCaml via returning [] or None.
  • OCaml Approach

    List.concat_map: ('a -> 'b list) -> 'a list -> 'b list (since 4.10) is the direct equivalent. Before 4.10: List.concat (List.map f xs). For optional results: List.filter_map: ('a -> 'b option) -> 'a list -> 'b list is more idiomatic than flat_map with option. String.split_on_char ' ' s |> List.concat_map (String.split_on_char '\n') chains multiple splits. For sequences: Seq.flat_map provides lazy flat_map.

    Full Source

    #![allow(clippy::all)]
    //! 259. Flattening with flat_map()
    //!
    //! `flat_map(f)` = `map(f).flatten()` — the iterator monad's bind operation.
    //! Map each element to zero-or-more outputs, collecting into a single flat sequence.
    
    /// Split each sentence into words, producing a flat list of all words.
    pub fn words_from_sentences<'a>(sentences: &[&'a str]) -> Vec<&'a str> {
        sentences
            .iter()
            .flat_map(|s| s.split_whitespace())
            .collect()
    }
    
    /// Expand each number n into the range 0..n, then flatten all ranges.
    pub fn expand_ranges(nums: &[i32]) -> Vec<i32> {
        nums.iter().flat_map(|&n| 0..n).collect()
    }
    
    /// Parse strings into integers, silently dropping failures (zero-output case).
    pub fn parse_valid(strings: &[&str]) -> Vec<i32> {
        strings.iter().flat_map(|s| s.parse::<i32>()).collect()
    }
    
    /// Extract all bytes from a slice of strings into a single flat byte sequence.
    pub fn bytes_from_words(words: &[&str]) -> Vec<u8> {
        words.iter().flat_map(|w| w.bytes()).collect()
    }
    
    /// Double only successfully-parsed integers, dropping non-numeric strings.
    pub fn parse_and_double(strings: &[&str]) -> Vec<i32> {
        strings
            .iter()
            .flat_map(|s| s.parse::<i32>().map(|n| n * 2))
            .collect()
    }
    
    /// Recursive equivalent: concat_map over a list, mirroring OCaml's List.concat_map.
    pub fn concat_map<A, B, F>(items: &[A], f: F) -> Vec<B>
    where
        F: Fn(&A) -> Vec<B>,
    {
        items.iter().flat_map(f).collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_words_from_sentences_basic() {
            let sentences = ["hello world", "foo bar"];
            assert_eq!(
                words_from_sentences(&sentences),
                vec!["hello", "world", "foo", "bar"]
            );
        }
    
        #[test]
        fn test_words_from_sentences_empty() {
            assert_eq!(words_from_sentences(&[]), Vec::<&str>::new());
        }
    
        #[test]
        fn test_expand_ranges() {
            // 0..1 = [0], 0..2 = [0,1], 0..3 = [0,1,2]
            assert_eq!(expand_ranges(&[1, 2, 3]), vec![0, 0, 1, 0, 1, 2]);
        }
    
        #[test]
        fn test_expand_ranges_with_zero() {
            // n=0 contributes nothing — zero-output case
            assert_eq!(expand_ranges(&[0, 2, 0]), vec![0, 1]);
        }
    
        #[test]
        fn test_parse_valid_filters_failures() {
            let strings = ["1", "two", "3", "four", "5"];
            assert_eq!(parse_valid(&strings), vec![1, 3, 5]);
        }
    
        #[test]
        fn test_parse_valid_all_fail() {
            let strings = ["abc", "def"];
            assert_eq!(parse_valid(&strings), Vec::<i32>::new());
        }
    
        #[test]
        fn test_bytes_from_words() {
            let words = ["hi", "ok"];
            let result = bytes_from_words(&words);
            assert_eq!(result, b"hiok");
        }
    
        #[test]
        fn test_parse_and_double() {
            let strings = ["1", "two", "3", "four", "5"];
            assert_eq!(parse_and_double(&strings), vec![2, 6, 10]);
        }
    
        #[test]
        fn test_concat_map_mirrors_ocaml() {
            // List.concat_map (fun n -> List.init n Fun.id) [1;2;3]
            let result = concat_map(&[1i32, 2, 3], |&n| (0..n).collect());
            assert_eq!(result, vec![0, 0, 1, 0, 1, 2]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_words_from_sentences_basic() {
            let sentences = ["hello world", "foo bar"];
            assert_eq!(
                words_from_sentences(&sentences),
                vec!["hello", "world", "foo", "bar"]
            );
        }
    
        #[test]
        fn test_words_from_sentences_empty() {
            assert_eq!(words_from_sentences(&[]), Vec::<&str>::new());
        }
    
        #[test]
        fn test_expand_ranges() {
            // 0..1 = [0], 0..2 = [0,1], 0..3 = [0,1,2]
            assert_eq!(expand_ranges(&[1, 2, 3]), vec![0, 0, 1, 0, 1, 2]);
        }
    
        #[test]
        fn test_expand_ranges_with_zero() {
            // n=0 contributes nothing — zero-output case
            assert_eq!(expand_ranges(&[0, 2, 0]), vec![0, 1]);
        }
    
        #[test]
        fn test_parse_valid_filters_failures() {
            let strings = ["1", "two", "3", "four", "5"];
            assert_eq!(parse_valid(&strings), vec![1, 3, 5]);
        }
    
        #[test]
        fn test_parse_valid_all_fail() {
            let strings = ["abc", "def"];
            assert_eq!(parse_valid(&strings), Vec::<i32>::new());
        }
    
        #[test]
        fn test_bytes_from_words() {
            let words = ["hi", "ok"];
            let result = bytes_from_words(&words);
            assert_eq!(result, b"hiok");
        }
    
        #[test]
        fn test_parse_and_double() {
            let strings = ["1", "two", "3", "four", "5"];
            assert_eq!(parse_and_double(&strings), vec![2, 6, 10]);
        }
    
        #[test]
        fn test_concat_map_mirrors_ocaml() {
            // List.concat_map (fun n -> List.init n Fun.id) [1;2;3]
            let result = concat_map(&[1i32, 2, 3], |&n| (0..n).collect());
            assert_eq!(result, vec![0, 0, 1, 0, 1, 2]);
        }
    }

    Deep Comparison

    OCaml vs Rust: Flattening with flat_map()

    Side-by-Side Code

    OCaml

    (* List.concat_map : ('a -> 'b list) -> 'a list -> 'b list *)
    
    (* Split sentences into words *)
    let words = List.concat_map String.split_on_char ' ' ["hello world"; "foo bar"]
    
    (* Expand each n into range 0..n-1 *)
    let expanded = List.concat_map (fun n -> List.init n Fun.id) [1; 2; 3]
    (* → [0; 0; 1; 0; 1; 2] *)
    
    (* Parse, silently drop failures *)
    let parsed = List.concat_map (fun s ->
      match int_of_string_opt s with
      | Some n -> [n * 2]
      | None -> []
    ) ["1"; "two"; "3"]
    (* → [2; 6] *)
    

    Rust (idiomatic — iterator adapter)

    // flat_map is lazy: no intermediate Vec allocated
    let words: Vec<&str> = ["hello world", "foo bar"]
        .iter()
        .flat_map(|s| s.split_whitespace())
        .collect();
    // → ["hello", "world", "foo", "bar"]
    
    let expanded: Vec<i32> = [1i32, 2, 3]
        .iter()
        .flat_map(|&n| 0..n)
        .collect();
    // → [0, 0, 1, 0, 1, 2]
    
    // Result/Option implements IntoIterator — failures yield zero elements
    let parsed: Vec<i32> = ["1", "two", "3"]
        .iter()
        .flat_map(|s| s.parse::<i32>().map(|n| n * 2))
        .collect();
    // → [2, 6]
    

    Rust (explicit — map + flatten)

    // flat_map(f) is exactly map(f).flatten()
    let words: Vec<&str> = ["hello world", "foo bar"]
        .iter()
        .map(|s| s.split_whitespace())
        .flatten()
        .collect();
    

    Type Signatures

    ConceptOCamlRust
    FunctionList.concat_map : ('a -> 'b list) -> 'a list -> 'b listIterator::flat_map<B, F>(self, f: F) -> FlatMap<…>
    Input element'aSelf::Item
    Output per element'b list (eager list)impl IntoIterator<Item = B> (any iterable, lazy)
    Collectimplicit — returns a listexplicit .collect::<Vec<_>>()
    Drop failuresmatch … \| None -> []Result/Option implement IntoIterator — zero items on failure

    Key Insights

  • Laziness: OCaml's List.concat_map builds intermediate lists eagerly; Rust's flat_map is a lazy adapter — nothing is allocated until .collect() is called, enabling efficient pipeline composition.
  • Zero-output via type system: In OCaml you explicitly return [] to emit nothing. In Rust, Result and Option implement IntoIterator with zero items on failure, so flat_map(|s| s.parse::<i32>()) is both a filter and a transform — no explicit match needed.
  • map + flatten identity: flat_map(f)map(f).flatten() in both languages. Rust exposes this decomposition directly as two chainable adapters, making the monad bind law transparent.
  • Any iterable output: OCaml's concat_map requires a list return. Rust's flat_map accepts any IntoIterator — ranges (0..n), slices, String::bytes(), str::split_whitespace() — without wrapping in a Vec first.
  • Monad bind: flat_map is the >>= (bind) of the iterator/list monad. Both OCaml and Rust use it to sequence "one-to-many" transformations, but Rust's lazy evaluation makes it composable with the rest of the iterator ecosystem at zero allocation cost.
  • When to Use Each Style

    **Use flat_map (idiomatic)** when the mapping function naturally returns something iterable — a range, a split string, a parsed result — and you want a single flat output without intermediate allocations.

    **Use map + flatten explicitly** when you already have a Vec<Vec<T>> or Vec<Option<T>> and simply need to flatten it; or when the two steps are clearer separated for readability.

    Exercises

  • Use .flat_map() to implement expand_template(template: &str, vars: &HashMap<&str, Vec<&str>>) -> Vec<String> that expands each {var} into all possible values.
  • Write parse_and_validate<T, E, F>(strings: &[&str], parse: F) -> Vec<T> where F returns Result<T, E>, silently dropping errors.
  • Implement ngrams(text: &str, n: usize) -> Vec<Vec<&str>> using .flat_map() to generate all n-gram windows across sentences.
  • Open Source Repos