Traverse with Result
Tutorial
The Problem
When processing a list of inputs that might each fail with an error, you often want to either collect all successful results or report the first error. This is traverse for Result: Vec<T> → (T → Result<U,E>) → Result<Vec<U>,E>. Rust's Iterator::collect::<Result<Vec<T>,E>>() is exactly this — it short-circuits on the first Err and returns it, or collects all Ok values. Real applications: batch parsing user inputs, processing CSV rows (fail on first bad row), executing a list of database updates (rollback on first failure), and validating a sequence of configuration values.
🎯 Learning Outcomes
Err, collects all Ok on successIterator::collect::<Result<Vec<U>,E>>() as the idiomatic Rust traverse for Resulttry_fold to understand the mechanicsCode Example
fn traverse_result<T, U, E, F: Fn(&T) -> Result<U, E>>(xs: &[T], f: F) -> Result<Vec<U>, E> {
xs.iter().map(f).collect() // Built-in!
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Idiomatic | collect::<Result<Vec<_>,_>>() | List.fold_right |
| Short-circuit | First Err terminates | Same |
| Error info | Preserved in Err(e) | Preserved |
| vs. Validated | Short-circuits | Accumulates all errors |
| Manual | try_fold with ? | Result.bind in fold |
| Type | Result<Vec<U>, E> | ('u list, 'e) result |
OCaml Approach
OCaml's traverse for Result: let traverse f xs = List.fold_right (fun x acc -> match f x, acc with Ok y, Ok ys -> Ok (y :: ys) | Error e, _ -> Error e | _, Error e -> Error e) xs (Ok []). Alternatively using Result.bind: let traverse f xs = List.fold_left (fun acc x -> acc |> Result.bind (fun ys -> f x |> Result.map (fun y -> ys @ [y]))) (Ok []) xs. OCaml's let* y = f x in Ok (y :: ys) with let%bind reads cleanly.
Full Source
#![allow(clippy::all)]
// Example 065: Traverse with Result
// Turn Vec<Result<T,E>> into Result<Vec<T>,E>
// Approach 1: Using collect (Rust's built-in traverse for Result!)
fn traverse_result<T, U, E, F: Fn(&T) -> Result<U, E>>(xs: &[T], f: F) -> Result<Vec<U>, E> {
xs.iter().map(f).collect()
}
// Approach 2: Using try_fold
fn traverse_result_fold<T, U, E, F: Fn(&T) -> Result<U, E>>(xs: &[T], f: F) -> Result<Vec<U>, E> {
xs.iter().try_fold(Vec::new(), |mut acc, x| {
acc.push(f(x)?);
Ok(acc)
})
}
// Approach 3: Sequence
fn sequence_result<T, E>(xs: Vec<Result<T, E>>) -> Result<Vec<T>, E> {
xs.into_iter().collect()
}
fn parse_positive(s: &&str) -> Result<i32, String> {
let n: i32 = s.parse().map_err(|_| format!("Not a number: {}", s))?;
if n <= 0 {
Err(format!("Not positive: {}", n))
} else {
Ok(n)
}
}
fn validate_username(s: &&str) -> Result<String, String> {
if s.len() < 3 {
Err("Too short".into())
} else if s.len() > 20 {
Err("Too long".into())
} else {
Ok(s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_traverse_all_ok() {
assert_eq!(
traverse_result(&["1", "2", "3"], parse_positive),
Ok(vec![1, 2, 3])
);
}
#[test]
fn test_traverse_parse_error() {
assert_eq!(
traverse_result(&["1", "bad", "3"], parse_positive),
Err("Not a number: bad".into())
);
}
#[test]
fn test_traverse_validation_error() {
assert_eq!(
traverse_result(&["1", "-2", "3"], parse_positive),
Err("Not positive: -2".into())
);
}
#[test]
fn test_traverse_empty() {
let empty: &[&str] = &[];
assert_eq!(traverse_result(empty, parse_positive), Ok(vec![]));
}
#[test]
fn test_fold_version() {
assert_eq!(
traverse_result_fold(&["1", "2"], parse_positive),
Ok(vec![1, 2])
);
assert_eq!(
traverse_result_fold(&["1", "bad"], parse_positive),
Err("Not a number: bad".into())
);
}
#[test]
fn test_sequence_ok() {
assert_eq!(
sequence_result::<i32, String>(vec![Ok(1), Ok(2), Ok(3)]),
Ok(vec![1, 2, 3])
);
}
#[test]
fn test_sequence_err() {
let rs: Vec<Result<i32, &str>> = vec![Ok(1), Err("e"), Ok(3)];
assert_eq!(sequence_result(rs), Err("e"));
}
#[test]
fn test_validate_usernames() {
assert_eq!(
traverse_result(&["alice", "bob"], validate_username),
Ok(vec!["alice".into(), "bob".into()])
);
assert_eq!(
traverse_result(&["alice", "ab"], validate_username),
Err("Too short".into())
);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_traverse_all_ok() {
assert_eq!(
traverse_result(&["1", "2", "3"], parse_positive),
Ok(vec![1, 2, 3])
);
}
#[test]
fn test_traverse_parse_error() {
assert_eq!(
traverse_result(&["1", "bad", "3"], parse_positive),
Err("Not a number: bad".into())
);
}
#[test]
fn test_traverse_validation_error() {
assert_eq!(
traverse_result(&["1", "-2", "3"], parse_positive),
Err("Not positive: -2".into())
);
}
#[test]
fn test_traverse_empty() {
let empty: &[&str] = &[];
assert_eq!(traverse_result(empty, parse_positive), Ok(vec![]));
}
#[test]
fn test_fold_version() {
assert_eq!(
traverse_result_fold(&["1", "2"], parse_positive),
Ok(vec![1, 2])
);
assert_eq!(
traverse_result_fold(&["1", "bad"], parse_positive),
Err("Not a number: bad".into())
);
}
#[test]
fn test_sequence_ok() {
assert_eq!(
sequence_result::<i32, String>(vec![Ok(1), Ok(2), Ok(3)]),
Ok(vec![1, 2, 3])
);
}
#[test]
fn test_sequence_err() {
let rs: Vec<Result<i32, &str>> = vec![Ok(1), Err("e"), Ok(3)];
assert_eq!(sequence_result(rs), Err("e"));
}
#[test]
fn test_validate_usernames() {
assert_eq!(
traverse_result(&["alice", "bob"], validate_username),
Ok(vec!["alice".into(), "bob".into()])
);
assert_eq!(
traverse_result(&["alice", "ab"], validate_username),
Err("Too short".into())
);
}
}
Deep Comparison
Comparison: Traverse with Result
Traverse
OCaml:
let rec traverse_result f = function
| [] -> Ok []
| x :: xs ->
match f x with
| Error e -> Error e
| Ok y -> match traverse_result f xs with
| Error e -> Error e
| Ok ys -> Ok (y :: ys)
Rust:
fn traverse_result<T, U, E, F: Fn(&T) -> Result<U, E>>(xs: &[T], f: F) -> Result<Vec<U>, E> {
xs.iter().map(f).collect() // Built-in!
}
Sequence
OCaml:
let sequence_result xs = traverse_result Fun.id xs
Rust:
fn sequence_result<T, E>(xs: Vec<Result<T, E>>) -> Result<Vec<T>, E> {
xs.into_iter().collect()
}
Exercises
traverse_result to parse a CSV line into typed fields, returning the first parse error with context.traverse_result: insert all rows or return the first constraint violation.traverse_result (first error) with traverse_validated (all errors) on the same input and show the difference.traverse_result that accumulates ALL errors using Validated internally, then converts to Result.traverse_result(xs, f) is equivalent to xs.iter().map(f).collect().