903-iterator-flat-map — Iterator flat_map
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
.flat_map(f) as the composition of map and flatten.flat_map(|s| s.parse::<i32>()) to parse-and-filter in one passList.concat_map and Haskell's concatMapCode 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<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..flat_map() is lazy; OCaml List.concat_map is eager.[] 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]);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Function | List.concat_map : ('a -> 'b list) -> 'a list -> 'b list | Iterator::flat_map<B, F>(self, f: F) -> FlatMap<…> |
| Input element | 'a | Self::Item |
| Output per element | 'b list (eager list) | impl IntoIterator<Item = B> (any iterable, lazy) |
| Collect | implicit — returns a list | explicit .collect::<Vec<_>>() |
| Drop failures | match … \| None -> [] | Result/Option implement IntoIterator — zero items on failure |
Key Insights
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.[] 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.flat_map(f) ≡ map(f).flatten() in both languages. Rust exposes this decomposition directly as two chainable adapters, making the monad bind law transparent.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.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
.flat_map() to implement expand_template(template: &str, vars: &HashMap<&str, Vec<&str>>) -> Vec<String> that expands each {var} into all possible values.parse_and_validate<T, E, F>(strings: &[&str], parse: F) -> Vec<T> where F returns Result<T, E>, silently dropping errors.ngrams(text: &str, n: usize) -> Vec<Vec<&str>> using .flat_map() to generate all n-gram windows across sentences.