303: Collecting Iterator<Result<T>> into Result<Vec<T>>
Tutorial Video
Text description (accessibility)
This video demonstrates the "303: Collecting Iterator<Result<T>> into Result<Vec<T>>" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Parsing a batch of inputs where each might fail presents a choice: fail fast on the first error, or collect all errors. Key difference from OCaml: 1. **Conciseness**: Rust's `collect::<Result<Vec<_>, _>>()` is a single method call; OCaml requires an explicit fold.
Tutorial
The Problem
Parsing a batch of inputs where each might fail presents a choice: fail fast on the first error, or collect all errors. The collect::<Result<Vec<T>, E>>() pattern implements fail-fast: if any element produces Err, the entire collection short-circuits and returns that Err. This is the most common pattern for validating a batch of inputs — parse all or fail on the first invalid one.
🎯 Learning Outcomes
collect::<Result<Vec<T>, E>>() short-circuits on the first Errfilter_map(|r| r.ok()) which silently drops errors? to propagate batch validation errors cleanlyCode Example
#![allow(clippy::all)]
//! # Collecting Iterator<Result<T>> into Result<Vec<T>>
//!
//! `collect::<Result<Vec<T>,E>>()` short-circuits on first Err.
/// Parse all strings - fails on first error
pub fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
inputs.iter().map(|s| s.parse::<i32>()).collect()
}
/// Sum parsed numbers - returns error if any parse fails
pub fn sum_all(inputs: &[&str]) -> Result<i32, std::num::ParseIntError> {
let nums: Vec<i32> = parse_all(inputs)?;
Ok(nums.iter().sum())
}
/// Process and transform - all-or-nothing
pub fn double_all(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
inputs
.iter()
.map(|s| s.parse::<i32>().map(|n| n * 2))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_all_ok() {
let result = parse_all(&["1", "2", "3"]);
assert_eq!(result.unwrap(), vec![1, 2, 3]);
}
#[test]
fn test_collect_with_err() {
let result = parse_all(&["1", "bad", "3"]);
assert!(result.is_err());
}
#[test]
fn test_sum_all_ok() {
let result = sum_all(&["10", "20", "30"]);
assert_eq!(result.unwrap(), 60);
}
#[test]
fn test_sum_all_err() {
let result = sum_all(&["10", "x", "30"]);
assert!(result.is_err());
}
#[test]
fn test_double_all() {
let result = double_all(&["1", "2", "3"]);
assert_eq!(result.unwrap(), vec![2, 4, 6]);
}
#[test]
fn test_empty_input() {
let result = parse_all(&[]);
assert_eq!(result.unwrap(), Vec::<i32>::new());
}
}Key Differences
collect::<Result<Vec<_>, _>>() is a single method call; OCaml requires an explicit fold.fold_right stops from the right — order matters.collect::<Result<Vec<_>>> gives all-or-nothing; partition(Result::is_ok) gives both lists — choose based on requirements.map transformation; the turbofish ::<Result<Vec<i32>, _>> is rarely needed.OCaml Approach
OCaml requires an explicit fold for this pattern:
let parse_all inputs =
List.fold_right (fun s acc ->
match int_of_string_opt s with
| None -> Error (Printf.sprintf "not a number: %s" s)
| Some n -> Result.map (fun lst -> n :: lst) acc
) inputs (Ok [])
The fold short-circuits naturally when acc is Error _ — matching Rust's behavior.
Full Source
#![allow(clippy::all)]
//! # Collecting Iterator<Result<T>> into Result<Vec<T>>
//!
//! `collect::<Result<Vec<T>,E>>()` short-circuits on first Err.
/// Parse all strings - fails on first error
pub fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
inputs.iter().map(|s| s.parse::<i32>()).collect()
}
/// Sum parsed numbers - returns error if any parse fails
pub fn sum_all(inputs: &[&str]) -> Result<i32, std::num::ParseIntError> {
let nums: Vec<i32> = parse_all(inputs)?;
Ok(nums.iter().sum())
}
/// Process and transform - all-or-nothing
pub fn double_all(inputs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
inputs
.iter()
.map(|s| s.parse::<i32>().map(|n| n * 2))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_all_ok() {
let result = parse_all(&["1", "2", "3"]);
assert_eq!(result.unwrap(), vec![1, 2, 3]);
}
#[test]
fn test_collect_with_err() {
let result = parse_all(&["1", "bad", "3"]);
assert!(result.is_err());
}
#[test]
fn test_sum_all_ok() {
let result = sum_all(&["10", "20", "30"]);
assert_eq!(result.unwrap(), 60);
}
#[test]
fn test_sum_all_err() {
let result = sum_all(&["10", "x", "30"]);
assert!(result.is_err());
}
#[test]
fn test_double_all() {
let result = double_all(&["1", "2", "3"]);
assert_eq!(result.unwrap(), vec![2, 4, 6]);
}
#[test]
fn test_empty_input() {
let result = parse_all(&[]);
assert_eq!(result.unwrap(), Vec::<i32>::new());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_all_ok() {
let result = parse_all(&["1", "2", "3"]);
assert_eq!(result.unwrap(), vec![1, 2, 3]);
}
#[test]
fn test_collect_with_err() {
let result = parse_all(&["1", "bad", "3"]);
assert!(result.is_err());
}
#[test]
fn test_sum_all_ok() {
let result = sum_all(&["10", "20", "30"]);
assert_eq!(result.unwrap(), 60);
}
#[test]
fn test_sum_all_err() {
let result = sum_all(&["10", "x", "30"]);
assert!(result.is_err());
}
#[test]
fn test_double_all() {
let result = double_all(&["1", "2", "3"]);
assert_eq!(result.unwrap(), vec![2, 4, 6]);
}
#[test]
fn test_empty_input() {
let result = parse_all(&[]);
assert_eq!(result.unwrap(), Vec::<i32>::new());
}
}
Deep Comparison
Collecting Results
| Concept | OCaml | Rust |
|---|---|---|
| All-or-nothing | Manual fold | .collect::<Result<Vec<_>, _>>() |
| Short-circuit | Manual | Automatic |
Exercises
Result<Vec<Row>, ParseError> and short-circuiting on the first malformed row.collect::<Result<Vec<_>>> with filter_map(Result::ok).collect::<Vec<_>>() on a mixed input — show they produce different results.collect::<Result<Vec<_>>>() in a function returning Result, then propagate the collection error with ?.