ExamplesBy LevelBy TopicLearning Paths
1009 Intermediate

1009-collecting-results — Collecting Results

Functional Programming

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

  • • Use .collect() to transform an iterator of Results into a single Result<Vec<T>, E>
  • • Understand the short-circuit behaviour and its implications for laziness
  • • Compare the collect() approach to try_fold and explicit loops
  • • Know when to use partition instead of collect when you want to keep partial results
  • • Appreciate how FromIterator<Result<T, E>> is implemented in the standard library
  • Code 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

  • Trait-based dispatch: Rust's collect is driven by FromIterator, a trait impl in std; OCaml needs explicit library functions or custom code.
  • Laziness: Rust iterators are lazy, so collect on a map pipeline does not build an intermediate Vec<Result>; OCaml List.map is strict.
  • Short-circuit guarantee: Rust's FromIterator<Result> is guaranteed to stop at first Err; OCaml implementations vary by library.
  • Type inference: Rust requires a turbofish or type annotation to select the 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

  • • Write sequence or traverse manually (fold + reverse)
  • • No stdlib function for Result list -> list Result before external libs
  • • Must be explicit about short-circuit behavior
  • • Libraries like Base or Lwt provide this
  • Rust Approach

  • iter.collect::<Result<Vec<T>, E>>() — one line, built-in
  • FromIterator trait impl handles the short-circuit
  • try_fold for more control over accumulation
  • • Type inference usually figures out the target type
  • Comparison Table

    AspectOCamlRust
    Built-inNo (manual sequence)Yes (collect())
    Short-circuitsMust implementAutomatic
    Type inferenceN/ADrives collect() target
    Traverse (map+collect)Manual traverse.map(f).collect()
    Empty inputOk []Ok(vec![])

    Exercises

  • Write a parse_all_keep_errors function that returns (Vec<i64>, Vec<String>) — all successes and all errors — using Iterator::partition.
  • Modify parse_all to skip errors silently with filter_map(|r| r.ok()) instead of short-circuiting. Compare the signatures.
  • Implement a collect_first_n_ok(inputs: &[&str], n: usize) -> Result<Vec<i64>, String> that succeeds only when at least n inputs parse successfully.
  • Open Source Repos