1010-partition-results — Partition Results
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
Iterator::partition to separate Ok and Err valuesfilter_map with .ok() or .err() to extract one side onlycollect::<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
partition produces Vec<Result<...>> requiring a second unwrap pass; fold does it in one pass. OCaml's partition_map is one-pass by design.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.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());
}
}#[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 sideRust Approach
Iterator::partition(Result::is_ok) splits into two Vec<Result>sunwrap/unwrap_err each side (safe because we just partitioned)filter_map(Result::ok) / filter_map(Result::err) for one-sided extractionComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Partition | List.partition is_ok | iter.partition(Result::is_ok) |
| Unwrap after | Manual pattern match | Result::unwrap (safe post-partition) |
| One-sided | List.filter_map | filter_map(Result::ok) |
| Performance | Two passes (partition + map) | Same |
| Use case | Collect all errors for reporting | Same |
Exercises
partition_results<T, E> function that takes Vec<Result<T, E>> and returns (Vec<T>, Vec<E>) without the intermediate Vec<Result<...>> step.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.