Nugget 13

Home

Flat and Flatten

map() gives you one output per input. But what if one word should produce multiple letters? One person produce multiple tags? That's when you need flat_map.

The Problem: Nested Iterators

Sometimes each input item produces multiple outputs. Using map gives you an iterator of iterators:

let words = ["hello", "world"];

// map: each word → its chars iterator → Vec of Vecs
let letters: Vec<Vec<char>> = words.iter()
    .map(|word| word.chars().collect::<Vec<_>>())
    .collect();
// [['h', 'e', 'l', 'l', 'o'], ['w', 'o', 'r', 'l', 'd']]

// But what if you just want a flat list of all characters?
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

You could collect and then flatten in two steps. Or use flat_map in one step.

flat_map() — Map Then Flatten

flat_map() is like map() but the closure returns an iterator, and the results are flattened into a single sequence:

let words = ["hello", "world"];

// flat_map: map each word to chars, then flatten everything
let letters: Vec<char> = words.iter()
    .flat_map(|word| word.chars())
    .collect();
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

// Another example: expand numbers into ranges
let expanded: Vec<i32> = [1, 3, 2].iter()
    .flat_map(|&n| 1..=n)
    .collect();
// 1..=1 → [1], 1..=3 → [1,2,3], 1..=2 → [1,2]
// Result: [1, 1, 2, 3, 1, 2]

The closure returns an iterator (not a collection), so nothing is allocated in between — each inner iterator produces items one at a time.

Practical: Words from Lines

A common pattern: split text into lines, then each line into words:

let poem = "the quick brown\nfox jumps over\nthe lazy dog";

// Each line → words, then collect everything flat
let all_words: Vec<&str> = poem
    .lines()
    .flat_map(|line| line.split_whitespace())
    .collect();

println!("{:?}", all_words);
// ["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]

Without flat_map you'd need nested loops. With it, you get a flat sequence in one pipeline.

flatten() — Just Flatten

flatten() is flat_map without the mapping step — it just removes one level of nesting:

let nested = vec![
    vec![1, 2, 3],
    vec![4, 5],
    vec![6, 7, 8, 9],
];

let flat: Vec<i32> = nested.iter()
    .flatten()          // each &Vec<i32> → &i32 items
    .copied()           // &i32 → i32
    .collect();
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

// flatten works on any nested iterator

flatten() with Option

One of the most useful applications: Option implements IntoIterator (0 or 1 items), so flatten() on an iterator of Options drops all Nones:

let items = vec![Some(1), None, Some(2), None, Some(3)];

let just_values: Vec<i32> = items.into_iter()
    .flatten()   // None → yields nothing, Some(x) → yields x
    .collect();
// [1, 2, 3]

// Equivalent to:
let just_values: Vec<i32> = items.into_iter()
    .filter_map(|x| x)
    .collect();
// filter_map is for when you need custom filtering + mapping

💡 filter_map for custom logic

Use filter_map() when you want to both filter and transform at once — return Some(result) for keep or None to skip: .filter_map(|x| x.parse().ok())

Performance: Lazy All the Way Down

Like all iterator adapters, flat_map() and flatten() are lazy. The inner iterators don't allocate — each item is produced on demand:

// Lazy: inner iterators are never fully materialized
let words = ["hello", "world"];
let mut flat = words.iter().flat_map(|w| w.chars());

assert_eq!(flat.next(), Some('h'));
assert_eq!(flat.next(), Some('e'));
// Only 2 chars produced so far — "world" hasn't started yet

// JS equivalent creates TWO intermediate arrays
// "hello".split('') → ['h','e','l','l','o']
// "world".split('') → ['w','o','r','l','d']
// then flattens → one array of 10 chars

📐 Zero-allocation flattening

In JavaScript, arr.flatMap(fn) is also lazy-ish in some engines, but the common pattern of .map().flat() creates intermediate arrays. Rust's flat_map is always a single pass with no intermediate allocations.

Key Takeaways