The Rust Expression Guide
Flattening and Expanding with flatten and flat_map
Last Updated: 2026-04-04
What this page is about
Rust code often produces nested structure. An iterator may yield iterators. A transformation may produce Option<Option<T>>. A parser may produce a list of lists. In those cases, the core question becomes: do you want to remove an existing layer of nesting, or produce nested structure and immediately flatten it as part of the transformation?
That is the distinction between flatten and flat_map.
This page explains how to reason about both methods, how they differ, and when they improve clarity. The focus is not on clever one-liners. It is on building accurate intuition so that nested structure feels manageable instead of mysterious.
The core mental model
A useful first distinction is this:
flattenremoves one layer of existing nestingflat_mapfirst maps each item into a nested structure, then flattens one layer
That means flatten is about structure you already have, while flat_map is about structure your transformation produces.
A tiny iterator example:
fn flattened() -> Vec<i32> {
vec![vec![1, 2], vec![3, 4]]
.into_iter()
.flatten()
.collect()
}Here the nested vectors already exist. flatten removes one level.
With flat_map:
fn expanded() -> Vec<char> {
["ab", "cd"]
.into_iter()
.flat_map(|s| s.chars())
.collect()
}Here the closure produces an iterator for each input item. flat_map then flattens those produced iterators into one stream.
Why this distinction matters
Without a clear mental model, flatten and flat_map can feel like interchangeable iterator magic. They are not.
If you already have nested structure, flatten is often the most direct way to remove it.
If the transformation itself yields a nested structure, flat_map often makes the intent clearer than a separate map(...).flatten().
This matters because code becomes easier to read when the structure of the pipeline matches the structure of the data. The point is not to shorten everything. The point is to describe whether nesting is being removed or introduced-and-immediately-removed.
Using `flatten` on iterators of iterators
The most common iterator use of flatten is when you already have an iterator whose items are themselves iterable.
fn all_numbers() -> Vec<i32> {
let groups = vec![vec![1, 2], vec![3], vec![4, 5]];
groups.into_iter().flatten().collect()
}Without flatten, you would often write nested loops:
fn all_numbers_loop() -> Vec<i32> {
let groups = vec![vec![1, 2], vec![3], vec![4, 5]];
let mut out = Vec::new();
for group in groups {
for value in group {
out.push(value);
}
}
out
}flatten is useful here because the nested structure already exists and the operation is simply to stream the inner items one after another.
Using `flatten` with `Option`-like structure
flatten is also useful when nested optionality appears.
fn flatten_option(input: Option<Option<u32>>) -> Option<u32> {
input.flatten()
}This removes one layer of Option nesting.
A more realistic example:
fn first_port(input: Option<&str>) -> Option<u16> {
input.map(|s| s.trim().parse::<u16>().ok()).flatten()
}This works because map produces Option<Option<u16>>, and flatten removes one layer.
That said, this specific shape is often better expressed with and_then:
fn first_port(input: Option<&str>) -> Option<u16> {
input.and_then(|s| s.trim().parse::<u16>().ok())
}The point is not that flatten is always the best choice. It is that it is the right conceptual tool when you already have nested optional structure.
Using `flat_map`
flat_map is for the case where each input item expands into zero, one, or many output items by producing something iterable.
fn letters(words: &[&str]) -> Vec<char> {
words.iter().flat_map(|word| word.chars()).collect()
}Each input word produces an iterator of characters. flat_map turns the whole thing into one flattened stream of characters.
Another example:
fn split_words(lines: &[&str]) -> Vec<&str> {
lines.iter().flat_map(|line| line.split_whitespace()).collect()
}This is often the clearest way to say: for each line, produce its words, then stream all the words together.
Removing structure versus producing structure
This distinction is the heart of the page.
If the nested structure already exists before your method call, you usually want flatten.
fn flatten_existing() -> Vec<i32> {
let nested = vec![vec![1, 2], vec![3, 4]];
nested.into_iter().flatten().collect()
}If your closure produces nested structure from each item, you usually want flat_map.
fn produce_and_flatten() -> Vec<char> {
["12", "34"]
.into_iter()
.flat_map(|s| s.chars())
.collect()
}A good practical question is: "Did the nesting already exist, or am I creating it as part of the transformation?"
Why `flat_map` is often clearer than `map(...).flatten()`
In many cases, map(...).flatten() and flat_map(...) can produce the same result.
fn with_map_flatten(lines: &[&str]) -> Vec<&str> {
lines.iter().map(|line| line.split_whitespace()).flatten().collect()
}
fn with_flat_map(lines: &[&str]) -> Vec<&str> {
lines.iter().flat_map(|line| line.split_whitespace()).collect()
}The second version is often clearer because it names the combined operation directly.
The code is not first "mapping" in the sense that the reader should think about the intermediate nested structure as an important value. It is really performing one conceptual step: expand each line into words and stream them together.
That is why flat_map often reads better when the nested structure exists only as a byproduct of transformation.
Using `flatten` with iterators over `Option`
Iterator flatten can also be useful when the iterator items are Option<T> values.
fn present_values(values: Vec<Option<u32>>) -> Vec<u32> {
values.into_iter().flatten().collect()
}This works because each Option<T> can be viewed as yielding zero or one item. flatten removes that layer and yields only present values.
A similar example with strings:
fn nonempty_names(values: Vec<Option<String>>) -> Vec<String> {
values.into_iter().flatten().collect()
}This is a nice example because it shows that flatten is not only for lists of lists. It also applies to other iterable-like nesting patterns.
Using `flat_map` to expand items
flat_map is particularly useful when one input item naturally expands into several output items.
fn csv_fields(lines: &[&str]) -> Vec<&str> {
lines.iter().flat_map(|line| line.split(',')).collect()
}Another example:
fn chars_from_nonempty_lines(lines: &[&str]) -> Vec<char> {
lines
.iter()
.filter(|line| !line.trim().is_empty())
.flat_map(|line| line.chars())
.collect()
}These examples are good because the expansion is the main story. Each item becomes a small stream of new items.
Meaningful examples
Example 1: flatten nested lists of ports.
fn all_ports(groups: Vec<Vec<u16>>) -> Vec<u16> {
groups.into_iter().flatten().collect()
}Example 2: flatten present values from optional entries.
fn enabled_levels(values: Vec<Option<u8>>) -> Vec<u8> {
values.into_iter().flatten().collect()
}Example 3: expand lines into words.
fn words(lines: &[&str]) -> Vec<&str> {
lines.iter().flat_map(|line| line.split_whitespace()).collect()
}Example 4: expand tags into characters.
fn tag_chars(tags: &[&str]) -> Vec<char> {
tags.iter().flat_map(|tag| tag.chars()).collect()
}Example 5: remove one layer of optional nesting.
fn flatten_level(input: Option<Option<String>>) -> Option<String> {
input.flatten()
}A parsing example
Suppose you have several lines, each of which may contain several comma-separated numbers.
fn parsed_numbers(lines: &[&str]) -> Vec<u32> {
lines
.iter()
.flat_map(|line| line.split(','))
.filter_map(|part| part.trim().parse::<u32>().ok())
.collect()
}This is a good example because it shows both ideas working together.
flat_mapexpands each line into fieldsfilter_mapthen keeps only the successfully parsed numbers
The expansion and the filtering are distinct steps, so each method has a clear role.
A configuration example
Configuration code often contains nested entry structures that benefit from flattening.
fn config_values(groups: Vec<Vec<String>>) -> Vec<String> {
groups.into_iter().flatten().collect()
}And when lines can contain multiple flags:
fn all_flags(lines: &[&str]) -> Vec<&str> {
lines
.iter()
.flat_map(|line| line.split_whitespace())
.filter(|flag| flag.starts_with("--"))
.collect()
}This is a good fit for flat_map because each line expands into a variable number of tokens, which are then filtered.
A request-processing example
Request-processing code often has nested optional or repeated fields.
fn selected_roles(input: Vec<Option<String>>) -> Vec<String> {
input.into_iter().flatten().collect()
}And when each selected role can expand into component permissions:
fn all_permissions(roles: &[&str]) -> Vec<&str> {
roles
.iter()
.flat_map(|role| match *role {
"admin" => vec!["read", "write", "delete"],
"editor" => vec!["read", "write"],
_ => vec!["read"],
})
.collect()
}This is a useful example because it shows flat_map as an expansion tool: each role yields a small collection of permissions, and the final stream is flattened.
When a manual loop is still better
flatten and flat_map are useful, but a manual loop is still often better when:
- the expansion logic is substantial enough to deserve its own named block
- the code needs side effects such as logging during expansion
- the nested structure is only part of a more complicated algorithm
- the resulting pipeline becomes hard to scan
For example:
fn parsed_with_logging(lines: &[&str]) -> Vec<u32> {
let mut out = Vec::new();
for line in lines {
for part in line.split(',') {
match part.trim().parse::<u32>() {
Ok(n) => out.push(n),
Err(_) => eprintln!("skipping invalid number: {part}"),
}
}
}
out
}If the inner work has its own story, a loop can be clearer than a compact pipeline.
When `flatten` is better than `flat_map`
Sometimes developers reach for flat_map even though the nested structure already exists and no transformation is needed.
fn unnecessary_flat_map(groups: Vec<Vec<u32>>) -> Vec<u32> {
groups.into_iter().flat_map(|group| group).collect()
}This works, but flatten is clearer:
fn better(groups: Vec<Vec<u32>>) -> Vec<u32> {
groups.into_iter().flatten().collect()
}If the closure is only returning the nested structure unchanged, that is a strong signal that flatten is the better name for the operation.
When `flat_map` is better than `flatten`
The opposite mistake also happens: using map(...).flatten() where flat_map(...) is really the operation being performed.
fn with_map_flatten(words: &[&str]) -> Vec<char> {
words.iter().map(|word| word.chars()).flatten().collect()
}This works, but flat_map often says more directly what is happening:
fn with_flat_map(words: &[&str]) -> Vec<char> {
words.iter().flat_map(|word| word.chars()).collect()
}When the nesting exists only because the transformation produces it, flat_map is usually the clearer choice.
One layer at a time
A very important detail is that flatten removes only one layer of nesting at a time.
fn one_layer() -> Vec<Vec<i32>> {
vec![vec![vec![1, 2]], vec![vec![3, 4]]]
.into_iter()
.flatten()
.collect()
}The result here is Vec<Vec<i32>>, not Vec<i32>.
That is important because it keeps your mental model honest. flatten is not magical recursive collapse. It removes one level. If more flattening is needed, the code should say so explicitly.
A small CLI example
Here is a small command-line example that expands comma-separated arguments into a single stream of parsed numbers.
use std::env;
fn main() {
let numbers: Vec<u32> = env::args()
.skip(1)
.flat_map(|arg| {
arg.split(',')
.map(str::trim)
.filter_map(|part| part.parse::<u32>().ok())
.collect::<Vec<_>>()
})
.collect();
println!("numbers: {numbers:?}");
}You can try it with:
cargo run -- 1,2,3 4,5 nope,6
cargo run -- 10 20,30
cargo runThis example is intentionally simple, but it shows the structure clearly:
- each argument expands into several parts
- valid numeric parts survive
- all produced numbers are flattened into one collection
A small project file for experimentation
You can experiment with the examples in a small project like this:
[package]
name = "flattening-and-expanding-with-flatten-and-flat-map"
version = "0.1.0"
edition = "2024"Then place one example at a time in src/main.rs and run:
cargo run
cargo fmt
cargo clippy
cargo testcargo fmt helps reveal pipeline structure. cargo clippy is useful for spotting cases where flat_map or flatten may express the code more directly than a more indirect chain.
Common mistakes
There are a few recurring mistakes around these methods.
First, treating flatten and flat_map as interchangeable.
Second, using flat_map when the nested structure already exists and no meaningful transformation is happening.
Third, using map(...).flatten() when flat_map(...) would communicate the combined operation more directly.
Fourth, forgetting that flatten removes only one layer at a time.
Fifth, forcing these methods into code where a manual loop would better communicate substantial inner logic.
The most important habit is to ask whether structure is being removed or produced.
Refactoring patterns to watch for
When reviewing code, these are strong signals that flatten or flat_map may help:
- nested loops exist only to stream inner items into one output collection
- a pipeline produces
Option<Option<T>>or iterators of iterators - code uses
map(...).flatten()and the mapping step is really an expansion - code uses
flat_map(|x| x)even though no transformation is happening - one input item naturally yields several output items
Typical before-and-after examples look like this:
fn before(groups: Vec<Vec<u32>>) -> Vec<u32> {
let mut out = Vec::new();
for group in groups {
for value in group {
out.push(value);
}
}
out
}
fn after(groups: Vec<Vec<u32>>) -> Vec<u32> {
groups.into_iter().flatten().collect()
}And for expansion:
fn before(lines: &[&str]) -> Vec<&str> {
let mut out = Vec::new();
for line in lines {
for word in line.split_whitespace() {
out.push(word);
}
}
out
}
fn after(lines: &[&str]) -> Vec<&str> {
lines.iter().flat_map(|line| line.split_whitespace()).collect()
}Key takeaways
flatten and flat_map help Rust code handle nested structure without unnecessary loops, but they solve different problems.
The main ideas from this page are:
- use
flattenwhen you already have nested structure and want to remove one layer - use
flat_mapwhen your transformation produces nested structure and you want to flatten it immediately flattenapplies not only to iterators of iterators, but also to shapes like iterators ofOption<T>andOption<Option<T>>flat_mapis especially useful when one input item naturally expands into several output itemsflat_mapis often clearer thanmap(...).flatten()when the nesting exists only because of the transformationflattenis often clearer thanflat_map(|x| x)when no transformation is happening- a manual loop is still better when the inner logic becomes the real story
Good iterator code gets easier to reason about when you can tell whether nesting is being removed or produced.
