ExamplesBy LevelBy TopicLearning Paths
303 Intermediate

303: Collecting Iterator<Result<T>> into Result<Vec<T>>

Functional Programming

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

  • • Understand that collect::<Result<Vec<T>, E>>() short-circuits on the first Err
  • • Use this pattern to validate that all inputs in a batch are well-formed
  • • Recognize the difference from filter_map(|r| r.ok()) which silently drops errors
  • • Combine with ? to propagate batch validation errors cleanly
  • Code 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

  • Conciseness: Rust's collect::<Result<Vec<_>, _>>() is a single method call; OCaml requires an explicit fold.
  • Short-circuit position: Rust stops at the first error in iteration order (left to right); the OCaml fold_right stops from the right — order matters.
  • vs partition: collect::<Result<Vec<_>>> gives all-or-nothing; partition(Result::is_ok) gives both lists — choose based on requirements.
  • Type inference: Rust infers the error type from the 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    All-or-nothingManual fold.collect::<Result<Vec<_>, _>>()
    Short-circuitManualAutomatic

    Exercises

  • Implement a CSV parser that parses each row's fields, collecting into Result<Vec<Row>, ParseError> and short-circuiting on the first malformed row.
  • Compare collect::<Result<Vec<_>>> with filter_map(Result::ok).collect::<Vec<_>>() on a mixed input — show they produce different results.
  • Use collect::<Result<Vec<_>>>() in a function returning Result, then propagate the collection error with ?.
  • Open Source Repos