1009-collecting-results — Collecting Results
Tutorial
The Problem
When processing a list of inputs that each produce a Result, you face a design decision: stop at the first error, or collect all values (or all errors). The "stop at first error" pattern is the most common, and Rust's standard library makes it a one-liner through the FromIterator implementation for Result.
Calling .collect::<Result<Vec<T>, E>>() on an Iterator<Item = Result<T, E>> will short-circuit at the first Err, returning it immediately. All Ok values are accumulated. If all items succeed, you get Ok(Vec<T>). This mirrors the sequence operation from functional programming.
🎯 Learning Outcomes
.collect() to transform an iterator of Results into a single Result<Vec<T>, E>collect() approach to try_fold and explicit loopsFromIterator<Result<T, E>> is implemented in the standard libraryCode Example
#![allow(clippy::all)]
// 1009: Collecting Results
// Iterator<Item=Result<T,E>> -> Result<Vec<T>, E> via collect()
fn parse_int(s: &str) -> Result<i64, String> {
s.parse::<i64>().map_err(|_| format!("bad: {}", s))
}
// Approach 1: collect() — the magic of FromIterator for Result
fn parse_all(inputs: &[&str]) -> Result<Vec<i64>, String> {
inputs.iter().map(|s| parse_int(s)).collect()
}
// Approach 2: Manual fold for clarity
fn parse_all_manual(inputs: &[&str]) -> Result<Vec<i64>, String> {
let mut results = Vec::new();
for s in inputs {
results.push(parse_int(s)?);
}
Ok(results)
}
// Approach 3: Using try_fold
fn parse_all_fold(inputs: &[&str]) -> Result<Vec<i64>, String> {
inputs.iter().try_fold(Vec::new(), |mut acc, s| {
acc.push(parse_int(s)?);
Ok(acc)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_all_ok() {
assert_eq!(parse_all(&["1", "2", "3"]), Ok(vec![1, 2, 3]));
}
#[test]
fn test_collect_first_error() {
let result = parse_all(&["1", "abc", "3"]);
assert_eq!(result, Err("bad: abc".to_string()));
}
#[test]
fn test_collect_empty() {
assert_eq!(parse_all(&[]), Ok(vec![]));
}
#[test]
fn test_manual_matches_collect() {
let inputs = &["10", "20", "30"];
assert_eq!(parse_all(inputs), parse_all_manual(inputs));
let bad = &["10", "x"];
assert!(parse_all_manual(bad).is_err());
}
#[test]
fn test_fold_matches_collect() {
let inputs = &["5", "10", "15"];
assert_eq!(parse_all(inputs), parse_all_fold(inputs));
}
#[test]
fn test_short_circuit_behavior() {
// collect() on Result short-circuits at first Err
let mut count = 0;
let result: Result<Vec<i64>, String> = ["1", "bad", "3"]
.iter()
.map(|s| {
count += 1;
parse_int(s)
})
.collect();
assert!(result.is_err());
// Iterator is lazy — may stop at error
assert!(count <= 3);
}
#[test]
fn test_single_element() {
assert_eq!(parse_all(&["42"]), Ok(vec![42]));
assert!(parse_all(&["xyz"]).is_err());
}
}Key Differences
collect is driven by FromIterator, a trait impl in std; OCaml needs explicit library functions or custom code.collect on a map pipeline does not build an intermediate Vec<Result>; OCaml List.map is strict.FromIterator<Result> is guaranteed to stop at first Err; OCaml implementations vary by library.Result<Vec<T>, E> interpretation; OCaml infers from context.OCaml Approach
OCaml lacks a direct equivalent but the pattern is expressible with List.fold_left:
let sequence results =
List.fold_left (fun acc r ->
match acc, r with
| Ok xs, Ok x -> Ok (xs @ [x])
| Error e, _ -> Error e
| _, Error e -> Error e
) (Ok []) results
The Base library provides List.map ~f |> Or_error.all for the same effect.
Full Source
#![allow(clippy::all)]
// 1009: Collecting Results
// Iterator<Item=Result<T,E>> -> Result<Vec<T>, E> via collect()
fn parse_int(s: &str) -> Result<i64, String> {
s.parse::<i64>().map_err(|_| format!("bad: {}", s))
}
// Approach 1: collect() — the magic of FromIterator for Result
fn parse_all(inputs: &[&str]) -> Result<Vec<i64>, String> {
inputs.iter().map(|s| parse_int(s)).collect()
}
// Approach 2: Manual fold for clarity
fn parse_all_manual(inputs: &[&str]) -> Result<Vec<i64>, String> {
let mut results = Vec::new();
for s in inputs {
results.push(parse_int(s)?);
}
Ok(results)
}
// Approach 3: Using try_fold
fn parse_all_fold(inputs: &[&str]) -> Result<Vec<i64>, String> {
inputs.iter().try_fold(Vec::new(), |mut acc, s| {
acc.push(parse_int(s)?);
Ok(acc)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_all_ok() {
assert_eq!(parse_all(&["1", "2", "3"]), Ok(vec![1, 2, 3]));
}
#[test]
fn test_collect_first_error() {
let result = parse_all(&["1", "abc", "3"]);
assert_eq!(result, Err("bad: abc".to_string()));
}
#[test]
fn test_collect_empty() {
assert_eq!(parse_all(&[]), Ok(vec![]));
}
#[test]
fn test_manual_matches_collect() {
let inputs = &["10", "20", "30"];
assert_eq!(parse_all(inputs), parse_all_manual(inputs));
let bad = &["10", "x"];
assert!(parse_all_manual(bad).is_err());
}
#[test]
fn test_fold_matches_collect() {
let inputs = &["5", "10", "15"];
assert_eq!(parse_all(inputs), parse_all_fold(inputs));
}
#[test]
fn test_short_circuit_behavior() {
// collect() on Result short-circuits at first Err
let mut count = 0;
let result: Result<Vec<i64>, String> = ["1", "bad", "3"]
.iter()
.map(|s| {
count += 1;
parse_int(s)
})
.collect();
assert!(result.is_err());
// Iterator is lazy — may stop at error
assert!(count <= 3);
}
#[test]
fn test_single_element() {
assert_eq!(parse_all(&["42"]), Ok(vec![42]));
assert!(parse_all(&["xyz"]).is_err());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_all_ok() {
assert_eq!(parse_all(&["1", "2", "3"]), Ok(vec![1, 2, 3]));
}
#[test]
fn test_collect_first_error() {
let result = parse_all(&["1", "abc", "3"]);
assert_eq!(result, Err("bad: abc".to_string()));
}
#[test]
fn test_collect_empty() {
assert_eq!(parse_all(&[]), Ok(vec![]));
}
#[test]
fn test_manual_matches_collect() {
let inputs = &["10", "20", "30"];
assert_eq!(parse_all(inputs), parse_all_manual(inputs));
let bad = &["10", "x"];
assert!(parse_all_manual(bad).is_err());
}
#[test]
fn test_fold_matches_collect() {
let inputs = &["5", "10", "15"];
assert_eq!(parse_all(inputs), parse_all_fold(inputs));
}
#[test]
fn test_short_circuit_behavior() {
// collect() on Result short-circuits at first Err
let mut count = 0;
let result: Result<Vec<i64>, String> = ["1", "bad", "3"]
.iter()
.map(|s| {
count += 1;
parse_int(s)
})
.collect();
assert!(result.is_err());
// Iterator is lazy — may stop at error
assert!(count <= 3);
}
#[test]
fn test_single_element() {
assert_eq!(parse_all(&["42"]), Ok(vec![42]));
assert!(parse_all(&["xyz"]).is_err());
}
}
Deep Comparison
Collecting Results — Comparison
Core Insight
Converting [Result<T,E>] to Result<[T], E> is a fundamental operation in both languages. Rust builds it into collect() via the type system; OCaml requires a manual combinator.
OCaml Approach
sequence or traverse manually (fold + reverse)Result list -> list Result before external libsBase or Lwt provide thisRust Approach
iter.collect::<Result<Vec<T>, E>>() — one line, built-inFromIterator trait impl handles the short-circuittry_fold for more control over accumulationComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Built-in | No (manual sequence) | Yes (collect()) |
| Short-circuits | Must implement | Automatic |
| Type inference | N/A | Drives collect() target |
| Traverse (map+collect) | Manual traverse | .map(f).collect() |
| Empty input | Ok [] | Ok(vec![]) |
Exercises
parse_all_keep_errors function that returns (Vec<i64>, Vec<String>) — all successes and all errors — using Iterator::partition.parse_all to skip errors silently with filter_map(|r| r.ok()) instead of short-circuiting. Compare the signatures.collect_first_n_ok(inputs: &[&str], n: usize) -> Result<Vec<i64>, String> that succeeds only when at least n inputs parse successfully.