304: Splitting Ok/Err with partition()
Tutorial Video
Text description (accessibility)
This video demonstrates the "304: Splitting Ok/Err with partition()" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. When processing a batch of inputs where some may fail, sometimes you want both the successes and the failures — not a short-circuit. Key difference from OCaml: 1. **OCaml 4.12+**: `List.partition_map` (new in 4.12) closely mirrors this Rust pattern with `Left`/`Right` discrimination.
Tutorial
The Problem
When processing a batch of inputs where some may fail, sometimes you want both the successes and the failures — not a short-circuit. Importing a CSV where invalid rows are logged and skipped, processing API responses where some fail and others succeed, or batch-validating records while collecting all errors. The partition(Result::is_ok) pattern collects all results in one pass, then extracts the Ok and Err values into separate vectors.
🎯 Learning Outcomes
partition(Result::is_ok) to split a Vec<Result<T, E>> into successes and failuresOk and Err variants after partitioningcollect::<Result<Vec<_>, _>>()fold for more control over multi-way result classificationCode Example
#![allow(clippy::all)]
//! # Splitting Ok/Err with partition()
//!
//! `partition(Result::is_ok)` collects ALL successes and ALL failures in one pass.
/// Partition results into successes and failures
pub fn partition_results<T: std::fmt::Debug, E: std::fmt::Debug>(
results: Vec<Result<T, E>>,
) -> (Vec<T>, Vec<E>) {
let (oks, errs): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
let ok_vals: Vec<T> = oks.into_iter().map(|r| r.unwrap()).collect();
let err_vals: Vec<E> = errs.into_iter().map(|r| r.unwrap_err()).collect();
(ok_vals, err_vals)
}
/// Parse all strings, collecting both successes and failures
pub fn parse_all_report(inputs: &[&str]) -> (Vec<i32>, Vec<String>) {
let results: Vec<Result<i32, String>> = inputs
.iter()
.map(|s| s.parse::<i32>().map_err(|_| s.to_string()))
.collect();
partition_results(results)
}
/// Alternative using fold for more control
pub fn partition_fold<T, E>(results: Vec<Result<T, E>>) -> (Vec<T>, Vec<E>) {
results
.into_iter()
.fold((vec![], vec![]), |(mut oks, mut errs), r| {
match r {
Ok(v) => oks.push(v),
Err(e) => errs.push(e),
}
(oks, errs)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partition_results() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Err("bad"), Ok(3)];
let (oks, errs) = partition_results(v);
assert_eq!(oks, vec![1, 3]);
assert_eq!(errs, vec!["bad"]);
}
#[test]
fn test_partition_all_ok() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Ok(2)];
let (oks, errs) = partition_results(v);
assert_eq!(oks, vec![1, 2]);
assert!(errs.is_empty());
}
#[test]
fn test_parse_all_report() {
let (nums, bad) = parse_all_report(&["1", "two", "3", "four"]);
assert_eq!(nums, vec![1, 3]);
assert_eq!(bad, vec!["two", "four"]);
}
#[test]
fn test_partition_fold() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Err("x"), Ok(2)];
let (oks, errs) = partition_fold(v);
assert_eq!(oks, vec![1, 2]);
assert_eq!(errs, vec!["x"]);
}
#[test]
fn test_empty_input() {
let v: Vec<Result<i32, &str>> = vec![];
let (oks, errs) = partition_results(v);
assert!(oks.is_empty());
assert!(errs.is_empty());
}
}Key Differences
List.partition_map (new in 4.12) closely mirrors this Rust pattern with Left/Right discrimination.partition + map(unwrap) uses two passes; itertools::partition_map does it in one.OCaml Approach
OCaml uses List.partition_map (OCaml 4.12+) or a fold:
let partition_results results =
List.partition_map (function
| Ok v -> Left v
| Error e -> Right e
) results
Earlier OCaml versions use List.fold_left with two accumulator lists.
Full Source
#![allow(clippy::all)]
//! # Splitting Ok/Err with partition()
//!
//! `partition(Result::is_ok)` collects ALL successes and ALL failures in one pass.
/// Partition results into successes and failures
pub fn partition_results<T: std::fmt::Debug, E: std::fmt::Debug>(
results: Vec<Result<T, E>>,
) -> (Vec<T>, Vec<E>) {
let (oks, errs): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
let ok_vals: Vec<T> = oks.into_iter().map(|r| r.unwrap()).collect();
let err_vals: Vec<E> = errs.into_iter().map(|r| r.unwrap_err()).collect();
(ok_vals, err_vals)
}
/// Parse all strings, collecting both successes and failures
pub fn parse_all_report(inputs: &[&str]) -> (Vec<i32>, Vec<String>) {
let results: Vec<Result<i32, String>> = inputs
.iter()
.map(|s| s.parse::<i32>().map_err(|_| s.to_string()))
.collect();
partition_results(results)
}
/// Alternative using fold for more control
pub fn partition_fold<T, E>(results: Vec<Result<T, E>>) -> (Vec<T>, Vec<E>) {
results
.into_iter()
.fold((vec![], vec![]), |(mut oks, mut errs), r| {
match r {
Ok(v) => oks.push(v),
Err(e) => errs.push(e),
}
(oks, errs)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partition_results() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Err("bad"), Ok(3)];
let (oks, errs) = partition_results(v);
assert_eq!(oks, vec![1, 3]);
assert_eq!(errs, vec!["bad"]);
}
#[test]
fn test_partition_all_ok() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Ok(2)];
let (oks, errs) = partition_results(v);
assert_eq!(oks, vec![1, 2]);
assert!(errs.is_empty());
}
#[test]
fn test_parse_all_report() {
let (nums, bad) = parse_all_report(&["1", "two", "3", "four"]);
assert_eq!(nums, vec![1, 3]);
assert_eq!(bad, vec!["two", "four"]);
}
#[test]
fn test_partition_fold() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Err("x"), Ok(2)];
let (oks, errs) = partition_fold(v);
assert_eq!(oks, vec![1, 2]);
assert_eq!(errs, vec!["x"]);
}
#[test]
fn test_empty_input() {
let v: Vec<Result<i32, &str>> = vec![];
let (oks, errs) = partition_results(v);
assert!(oks.is_empty());
assert!(errs.is_empty());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partition_results() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Err("bad"), Ok(3)];
let (oks, errs) = partition_results(v);
assert_eq!(oks, vec![1, 3]);
assert_eq!(errs, vec!["bad"]);
}
#[test]
fn test_partition_all_ok() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Ok(2)];
let (oks, errs) = partition_results(v);
assert_eq!(oks, vec![1, 2]);
assert!(errs.is_empty());
}
#[test]
fn test_parse_all_report() {
let (nums, bad) = parse_all_report(&["1", "two", "3", "four"]);
assert_eq!(nums, vec![1, 3]);
assert_eq!(bad, vec!["two", "four"]);
}
#[test]
fn test_partition_fold() {
let v: Vec<Result<i32, &str>> = vec![Ok(1), Err("x"), Ok(2)];
let (oks, errs) = partition_fold(v);
assert_eq!(oks, vec![1, 2]);
assert_eq!(errs, vec!["x"]);
}
#[test]
fn test_empty_input() {
let v: Vec<Result<i32, &str>> = vec![];
let (oks, errs) = partition_results(v);
assert!(oks.is_empty());
assert!(errs.is_empty());
}
}
Deep Comparison
Partition Results
| Concept | Rust |
|---|---|
| Split | partition(Result::is_ok) |
| Extract Ok | .flatten() or .map(unwrap) |
| Extract Err | .map(unwrap_err) |
Exercises
classify_results<T, E> function that takes a Vec<Result<T, E>> and returns three groups: successes, retryable errors, and permanent errors (based on error kind).partition(Result::is_ok) followed by unwrap() versus a single fold that accumulates directly — which avoids more intermediate allocations?