618: Traversal for Collection Optics
Difficulty: 5 Level: Master Traverse a collection with an effectful function โ short-circuit on `None`/`Err`, collect all results into `Option<Vec<T>>` or `Result<Vec<T>, E>`.The Problem This Solves
You have a `Vec<String>` that represents user-provided input. You want to parse every element into `i32`. If any element fails to parse, the whole operation should fail. You want a `Vec<i32>` on success, or `None` / an error on failure. The naive approach: iterate, collect errors separately, handle them after:let mut results = Vec::new();
let mut failed = false;
for s in &strs {
match s.parse::<i32>() {
Ok(n) => results.push(n),
Err(_) => { failed = true; break; }
}
}
if failed { return None; }
// results is Vec<i32> if we made it here
Or with nested iterators:
let parsed: Vec<Option<i32>> = strs.iter().map(|s| s.parse().ok()).collect();
// Now you have Vec<Option<i32>> โ you need to flip it to Option<Vec<i32>>
// How? Another pass, more boilerplate
let result: Option<Vec<i32>> = if parsed.iter().all(|x| x.is_some()) {
Some(parsed.into_iter().map(|x| x.unwrap()).collect())
} else {
None
};
This pattern โ apply a fallible function to every element, short-circuit on first failure, collect results โ is exactly what traversal is. It appears constantly in data validation, parsing pipelines, and batch processing. This exists to solve exactly that pain.
The Intuition
A Traversal is a Lens that focuses on multiple targets at once. Where a Lens reaches one field, a Traversal reaches all elements of a `Vec`, or all `Some` values in a `Vec<Option<T>>`, or all leaves of a tree. The key operation is `traverse`: apply an effectful function to each target and collect results. "Effectful" means the function returns a wrapper type โ `Option<B>`, `Result<B, E>`, `Vec<B>`. The magic is in how results are collected:- `traverse` with `Option`: `Vec<A>` + `fn(A) โ Option<B>` โ `Option<Vec<B>>`
- `traverse` with `Result`: `Vec<A>` + `fn(A) โ Result<B, E>` โ `Result<Vec<B>, E>`
Vec<A> + fn(A) -> Option<B>
โ traverse
Option<Vec<B>> โ Some([b1, b2, b3]) or None on first failure
In Rust, this is powered by a hidden trick: `Iterator::collect::<Option<Vec<B>>>()` already does this! Collecting an iterator of `Option<B>` into `Option<Vec<B>>` short-circuits on `None`. Traversal just makes this pattern explicit and composable.
How It Works in Rust
// Step 1: The fundamental traverse operation โ Option effect
fn traverse_opt<A, B>(xs: Vec<A>, f: impl Fn(A) -> Option<B>) -> Option<Vec<B>> {
xs.into_iter()
.map(f) // Iterator<Item = Option<B>>
.collect() // collect() on Iterator<Item=Option<B>> into Option<Vec<B>>
// โ this is the key: collect() knows to short-circuit!
}
// Works because Rust's FromIterator for Option short-circuits on None
traverse_opt(vec!["1", "2", "3"], |s| s.parse::<i32>().ok()); // Some([1, 2, 3])
traverse_opt(vec!["1", "x", "3"], |s| s.parse::<i32>().ok()); // None โ "x" fails
// Step 2: Same pattern for Result โ get an error message instead of None
fn traverse_result<A, B, E>(xs: Vec<A>, f: impl Fn(A) -> Result<B, E>) -> Result<Vec<B>, E> {
xs.into_iter().map(f).collect() // Same trick! collect() short-circuits on Err
}
// Returns first error encountered:
traverse_result(["1.5", "abc"].to_vec(), |s| {
s.parse::<f64>().map_err(|_| format!("bad float: {}", s))
}); // Err("bad float: abc")
// Step 3: Traverse a nested structure (matrix)
fn traverse_matrix<A: Clone, B>(
m: Vec<Vec<A>>,
f: impl Fn(A) -> Option<B> + Clone,
) -> Option<Vec<Vec<B>>> {
// outer traverse: for each row, inner traverse: for each element
traverse_opt(m, |row| traverse_opt(row, f.clone()))
// If any element in any row fails โ None for the whole matrix
}
let matrix = vec![vec!["1", "2"], vec!["3", "4"]];
traverse_matrix(matrix, |s| s.parse::<i32>().ok());
// Some([[1, 2], [3, 4]])
// Step 4: Traversal as filter_map โ "Prism traversal"
// Collect only the values that match a pattern (like a Prism over a collection)
fn collect_values<A: Clone, B>(xs: &[A], prism: impl Fn(&A) -> Option<B>) -> Vec<B> {
xs.iter().filter_map(prism).collect()
// Different from traverse_opt: here None means "skip this element", not "fail"
}
enum Json { Num(f64), Str(String), Null }
let jsons = vec![Json::Num(1.0), Json::Str("hi".into()), Json::Num(2.0), Json::Null];
let nums = collect_values(&jsons, |j| match j { Json::Num(n) => Some(*n), _ => None });
// [1.0, 2.0] โ strings and nulls filtered out
// Step 5: Traverse nested Option<Vec<A>>
fn traverse_nested<A, B>(opt: Option<Vec<A>>, f: impl Fn(A) -> Option<B>) -> Option<Vec<B>> {
opt.and_then(|xs| traverse_opt(xs, f))
// None input โ None output
// Some(xs) โ traverse xs โ Option<Vec<B>>
}
What This Unlocks
- Data validation pipelines โ parse and validate every field in a batch of records: `traverse_result(records, validate)` gives you either all-valid data or the first error, without manual error-accumulation code.
- API response handling โ deserialising a `Vec<RawJson>` into `Vec<ParsedItem>` is exactly traversal: `traverse_opt(raw_items, parse_item)`. One `None` means "malformed response."
- The `Option<Vec<T>>` โ `Vec<Option<T>>` flip โ knowing that `collect::<Option<Vec<T>>>()` does the flip explains a class of iterator patterns that are otherwise confusing.
Key Differences
| Concept | OCaml | Rust | |
|---|---|---|---|
| `traverse` | `List.map f xs \ | > sequence` or direct `List.filter_map` | `iter().map(f).collect::<Option<Vec<_>>>()` |
| Short-circuit on `None` | Applicative `sequence` over `option` monad | Built into `FromIterator` implementation for `Option<Vec<B>>` | |
| `traverse` with `Result` | Same: `sequence` over `result` applicative | `iter().map(f).collect::<Result<Vec<_>, E>>()` | |
| Filter-map (Prism traversal) | `List.filter_map` | `iter().filter_map(prism).collect()` | |
| Applicative requirement | Explicit: `traverse` requires `Applicative` constraint | Implicit: `FromIterator` trait handles the collection semantics |
// Traversal: optic focusing on multiple values with effects
// Traverse a Vec with Option effect (sequence + map)
fn traverse_opt<A, B>(xs: Vec<A>, f: impl Fn(A) -> Option<B>) -> Option<Vec<B>> {
xs.into_iter().map(f).collect()
}
// Traverse with Result effect
fn traverse_result<A, B, E>(xs: Vec<A>, f: impl Fn(A) -> Result<B, E>) -> Result<Vec<B>, E> {
xs.into_iter().map(f).collect()
}
// Traverse nested structure
fn traverse_matrix<A: Clone, B>(
m: Vec<Vec<A>>,
f: impl Fn(A) -> Option<B> + Clone,
) -> Option<Vec<Vec<B>>> {
traverse_opt(m, |row| traverse_opt(row, f.clone()))
}
// Practical traversal: parse all fields
fn parse_all_ints(strs: &[&str]) -> Option<Vec<i32>> {
traverse_opt(strs.to_vec(), |s| s.parse::<i32>().ok())
}
fn parse_all_floats(strs: &[&str]) -> Result<Vec<f64>, String> {
traverse_result(strs.to_vec(), |s| {
s.parse::<f64>().map_err(|e| format!("{}: {}", s, e))
})
}
// Traversal as filter_map (some targets may be missing)
fn collect_values<A: Clone, B>(xs: &[A], prism: impl Fn(&A) -> Option<B>) -> Vec<B> {
xs.iter().filter_map(prism).collect()
}
// Traversal over nested Option
fn traverse_nested<A, B>(opt: Option<Vec<A>>, f: impl Fn(A) -> Option<B>) -> Option<Vec<B>> {
opt.and_then(|xs| traverse_opt(xs, f))
}
fn main() {
println!("parse [1,2,3]: {:?}", parse_all_ints(&["1","2","3"]));
println!("parse [1,x,3]: {:?}", parse_all_ints(&["1","x","3"]));
println!("parse floats: {:?}", parse_all_floats(&["1.5","2.7","3.14"]));
println!("parse bad: {:?}", parse_all_floats(&["1.5","abc"]));
let matrix = vec![vec!["1","2"],vec!["3","4"]];
let parsed = traverse_matrix(matrix, |s| s.parse::<i32>().ok());
println!("matrix: {:?}", parsed);
#[derive(Debug,Clone)]
enum Json { Num(f64), Str(String), Null }
let jsons = vec![Json::Num(1.0), Json::Str("hi".into()), Json::Num(2.0), Json::Null];
let nums: Vec<f64> = collect_values(&jsons, |j| match j { Json::Num(n)=>Some(*n), _=>None });
println!("nums from json: {:?}", nums);
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn traverse_ok() { assert_eq!(parse_all_ints(&["1","2","3"]), Some(vec![1,2,3])); }
#[test] fn traverse_fail(){ assert_eq!(parse_all_ints(&["1","x","3"]), None); }
#[test] fn traverse_mat() {
let m = vec![vec!["1","2"],vec!["3","4"]];
assert_eq!(traverse_matrix(m, |s|s.parse::<i32>().ok()), Some(vec![vec![1,2],vec![3,4]]));
}
}
(* Traversal via functor mapping in OCaml *)
(* Traverse a list with option effects *)
let traverse_opt f xs =
let rec go = function
| [] -> Some []
| x :: xs ->
match (f x, go xs) with
| (Some y, Some ys) -> Some (y :: ys)
| _ -> None
in go xs
(* Traverse a list collecting all results or failing *)
let parse_all_ints strings =
traverse_opt (fun s -> try Some (int_of_string s) with _ -> None) strings
(* Traverse nested structure *)
type 'a matrix = 'a list list
let traverse_matrix f m =
traverse_opt (traverse_opt f) m
let () =
(match parse_all_ints ["1";"2";"3"] with
| Some xs -> Printf.printf "parsed: %s\n" (String.concat "," (List.map string_of_int xs))
| None -> Printf.printf "failed\n");
(match parse_all_ints ["1";"x";"3"] with
| Some xs -> ignore xs
| None -> Printf.printf "parse failed (expected)\n")