ExamplesBy LevelBy TopicLearning Paths
1010 Intermediate

1010-partition-results — Partition Results

Functional Programming

Tutorial

The Problem

When processing a batch of fallible operations, you often want to continue rather than abort at the first error. A log ingestion pipeline, for example, should record malformed lines separately rather than crashing the entire run. This requires splitting an iterator of Result values into two separate collections: the successes and the failures.

Rust's Iterator::partition splits any iterator into two Vecs based on a predicate. Combined with Result::is_ok and Result::unwrap/Result::unwrap_err, this gives a clean batch-error-handling pattern without imperative loops.

🎯 Learning Outcomes

  • • Use Iterator::partition to separate Ok and Err values
  • • Apply filter_map with .ok() or .err() to extract one side only
  • • Compare partition, fold-based accumulation, and filter_map approaches
  • • Understand the use case for tolerant batch processing versus fail-fast
  • • Know when to choose partition over collect::<Result<Vec<T>, E>>()
  • Code Example

    #![allow(clippy::all)]
    // 1010: Partition Results
    // Separate Ok and Err values using Iterator::partition
    
    fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>().map_err(|_| format!("bad: {}", s))
    }
    
    // Approach 1: partition into two Vecs of Results, then unwrap
    fn partition_results(inputs: &[&str]) -> (Vec<i64>, Vec<String>) {
        let (oks, errs): (Vec<Result<i64, String>>, Vec<Result<i64, String>>) =
            inputs.iter().map(|s| parse_int(s)).partition(Result::is_ok);
    
        (
            oks.into_iter().map(Result::unwrap).collect(),
            errs.into_iter().map(Result::unwrap_err).collect(),
        )
    }
    
    // Approach 2: Single fold into two accumulators
    fn partition_fold(inputs: &[&str]) -> (Vec<i64>, Vec<String>) {
        inputs.iter().map(|s| parse_int(s)).fold(
            (Vec::new(), Vec::new()),
            |(mut oks, mut errs), result| {
                match result {
                    Ok(v) => oks.push(v),
                    Err(e) => errs.push(e),
                }
                (oks, errs)
            },
        )
    }
    
    // Approach 3: Using filter_map for just one side
    fn only_successes(inputs: &[&str]) -> Vec<i64> {
        inputs.iter().filter_map(|s| parse_int(s).ok()).collect()
    }
    
    fn only_errors(inputs: &[&str]) -> Vec<String> {
        inputs.iter().filter_map(|s| parse_int(s).err()).collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partition_mixed() {
            let (oks, errs) = partition_results(&["1", "abc", "3", "def", "5"]);
            assert_eq!(oks, vec![1, 3, 5]);
            assert_eq!(errs, vec!["bad: abc", "bad: def"]);
        }
    
        #[test]
        fn test_partition_all_ok() {
            let (oks, errs) = partition_results(&["1", "2", "3"]);
            assert_eq!(oks, vec![1, 2, 3]);
            assert!(errs.is_empty());
        }
    
        #[test]
        fn test_partition_all_err() {
            let (oks, errs) = partition_results(&["a", "b", "c"]);
            assert!(oks.is_empty());
            assert_eq!(errs.len(), 3);
        }
    
        #[test]
        fn test_fold_matches_partition() {
            let inputs = &["1", "abc", "3"];
            assert_eq!(partition_results(inputs), partition_fold(inputs));
        }
    
        #[test]
        fn test_filter_map_successes() {
            assert_eq!(only_successes(&["1", "x", "3"]), vec![1, 3]);
        }
    
        #[test]
        fn test_filter_map_errors() {
            assert_eq!(only_errors(&["1", "x", "3"]), vec!["bad: x"]);
        }
    
        #[test]
        fn test_empty_input() {
            let (oks, errs) = partition_results(&[]);
            assert!(oks.is_empty());
            assert!(errs.is_empty());
        }
    }

    Key Differences

  • Two-pass vs one-pass: Rust's partition produces Vec<Result<...>> requiring a second unwrap pass; fold does it in one pass. OCaml's partition_map is one-pass by design.
  • Consuming iterators: Rust's iterator is consumed by partition; in OCaml, lists are persistent and can be re-processed freely.
  • **filter_map ergonomics**: Rust's .ok() and .err() methods make one-sided extraction concise; OCaml needs a lambda or function composition.
  • Type safety on unwrap: In Rust, calling Result::unwrap after partition(Result::is_ok) is technically safe but not statically verified; OCaml's pattern match is exhaustive.
  • OCaml Approach

    OCaml's List.partition is analogous:

    let partition_results results =
      let (oks, errs) = List.partition Result.is_ok results in
      (List.filter_map Result.to_option oks,
       List.filter_map (function Error e -> Some e | _ -> None) errs)
    

    OCaml's Base library provides List.partition_map which collapses the two steps into one pass using a First/Second variant.

    Full Source

    #![allow(clippy::all)]
    // 1010: Partition Results
    // Separate Ok and Err values using Iterator::partition
    
    fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>().map_err(|_| format!("bad: {}", s))
    }
    
    // Approach 1: partition into two Vecs of Results, then unwrap
    fn partition_results(inputs: &[&str]) -> (Vec<i64>, Vec<String>) {
        let (oks, errs): (Vec<Result<i64, String>>, Vec<Result<i64, String>>) =
            inputs.iter().map(|s| parse_int(s)).partition(Result::is_ok);
    
        (
            oks.into_iter().map(Result::unwrap).collect(),
            errs.into_iter().map(Result::unwrap_err).collect(),
        )
    }
    
    // Approach 2: Single fold into two accumulators
    fn partition_fold(inputs: &[&str]) -> (Vec<i64>, Vec<String>) {
        inputs.iter().map(|s| parse_int(s)).fold(
            (Vec::new(), Vec::new()),
            |(mut oks, mut errs), result| {
                match result {
                    Ok(v) => oks.push(v),
                    Err(e) => errs.push(e),
                }
                (oks, errs)
            },
        )
    }
    
    // Approach 3: Using filter_map for just one side
    fn only_successes(inputs: &[&str]) -> Vec<i64> {
        inputs.iter().filter_map(|s| parse_int(s).ok()).collect()
    }
    
    fn only_errors(inputs: &[&str]) -> Vec<String> {
        inputs.iter().filter_map(|s| parse_int(s).err()).collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partition_mixed() {
            let (oks, errs) = partition_results(&["1", "abc", "3", "def", "5"]);
            assert_eq!(oks, vec![1, 3, 5]);
            assert_eq!(errs, vec!["bad: abc", "bad: def"]);
        }
    
        #[test]
        fn test_partition_all_ok() {
            let (oks, errs) = partition_results(&["1", "2", "3"]);
            assert_eq!(oks, vec![1, 2, 3]);
            assert!(errs.is_empty());
        }
    
        #[test]
        fn test_partition_all_err() {
            let (oks, errs) = partition_results(&["a", "b", "c"]);
            assert!(oks.is_empty());
            assert_eq!(errs.len(), 3);
        }
    
        #[test]
        fn test_fold_matches_partition() {
            let inputs = &["1", "abc", "3"];
            assert_eq!(partition_results(inputs), partition_fold(inputs));
        }
    
        #[test]
        fn test_filter_map_successes() {
            assert_eq!(only_successes(&["1", "x", "3"]), vec![1, 3]);
        }
    
        #[test]
        fn test_filter_map_errors() {
            assert_eq!(only_errors(&["1", "x", "3"]), vec!["bad: x"]);
        }
    
        #[test]
        fn test_empty_input() {
            let (oks, errs) = partition_results(&[]);
            assert!(oks.is_empty());
            assert!(errs.is_empty());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partition_mixed() {
            let (oks, errs) = partition_results(&["1", "abc", "3", "def", "5"]);
            assert_eq!(oks, vec![1, 3, 5]);
            assert_eq!(errs, vec!["bad: abc", "bad: def"]);
        }
    
        #[test]
        fn test_partition_all_ok() {
            let (oks, errs) = partition_results(&["1", "2", "3"]);
            assert_eq!(oks, vec![1, 2, 3]);
            assert!(errs.is_empty());
        }
    
        #[test]
        fn test_partition_all_err() {
            let (oks, errs) = partition_results(&["a", "b", "c"]);
            assert!(oks.is_empty());
            assert_eq!(errs.len(), 3);
        }
    
        #[test]
        fn test_fold_matches_partition() {
            let inputs = &["1", "abc", "3"];
            assert_eq!(partition_results(inputs), partition_fold(inputs));
        }
    
        #[test]
        fn test_filter_map_successes() {
            assert_eq!(only_successes(&["1", "x", "3"]), vec![1, 3]);
        }
    
        #[test]
        fn test_filter_map_errors() {
            assert_eq!(only_errors(&["1", "x", "3"]), vec!["bad: x"]);
        }
    
        #[test]
        fn test_empty_input() {
            let (oks, errs) = partition_results(&[]);
            assert!(oks.is_empty());
            assert!(errs.is_empty());
        }
    }

    Deep Comparison

    Partition Results — Comparison

    Core Insight

    collect() short-circuits at the first error. When you want ALL successes AND all failures, you need partition — it processes every element.

    OCaml Approach

  • List.partition with a predicate, then unwrap each side
  • • Fold-based approach accumulates into two lists
  • • Must reverse lists after fold (cons builds in reverse)
  • Rust Approach

  • Iterator::partition(Result::is_ok) splits into two Vec<Result>s
  • • Then unwrap/unwrap_err each side (safe because we just partitioned)
  • filter_map(Result::ok) / filter_map(Result::err) for one-sided extraction
  • • Fold approach is also idiomatic
  • Comparison Table

    AspectOCamlRust
    PartitionList.partition is_okiter.partition(Result::is_ok)
    Unwrap afterManual pattern matchResult::unwrap (safe post-partition)
    One-sidedList.filter_mapfilter_map(Result::ok)
    PerformanceTwo passes (partition + map)Same
    Use caseCollect all errors for reportingSame

    Exercises

  • Write a generic partition_results<T, E> function that takes Vec<Result<T, E>> and returns (Vec<T>, Vec<E>) without the intermediate Vec<Result<...>> step.
  • Extend the example to count how many items of each type were processed and return the counts alongside the partitioned vectors.
  • Implement a take_while_ok function that returns all leading Ok values from an iterator and stops at the first Err, returning both the values and the error.
  • Open Source Repos