ExamplesBy LevelBy TopicLearning Paths
304 Intermediate

304: Splitting Ok/Err with partition()

Functional Programming

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

  • • Use partition(Result::is_ok) to split a Vec<Result<T, E>> into successes and failures
  • • Extract values from Ok and Err variants after partitioning
  • • Recognize this as the "harvest all results" counterpart to collect::<Result<Vec<_>, _>>()
  • • Use fold for more control over multi-way result classification
  • Code 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

  • OCaml 4.12+: List.partition_map (new in 4.12) closely mirrors this Rust pattern with Left/Right discrimination.
  • Two passes in Rust: Rust's partition + map(unwrap) uses two passes; itertools::partition_map does it in one.
  • Error collection: This pattern enables collecting all validation errors at once, which is better UX than "fix one error, retry, discover the next error".
  • Ordering: Both approaches preserve the relative order of elements within each group.
  • 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptRust
    Splitpartition(Result::is_ok)
    Extract Ok.flatten() or .map(unwrap)
    Extract Err.map(unwrap_err)

    Exercises

  • Parse a CSV file where each line may fail, collecting successfully parsed rows and all parse errors into separate lists.
  • Implement a 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).
  • Compare memory usage of partition(Result::is_ok) followed by unwrap() versus a single fold that accumulates directly — which avoids more intermediate allocations?
  • Open Source Repos