ExamplesBy LevelBy TopicLearning Paths
892 Intermediate

892-partition-iterator — Partition Iterator

Functional Programming

Tutorial

The Problem

Splitting a collection into groups based on a predicate is a fundamental data classification operation. Database query engines partition rows into matching and non-matching sets. Compilers partition tokens into valid and invalid. Trading systems partition orders into buy and sell. The naive approach uses two separate filter passes, scanning the data twice. Iterator::partition does this in one pass, returning two collections simultaneously. OCaml has List.partition. Python's itertools requires two passes. This single-pass property is critical for large data sets or when the source can only be consumed once.

🎯 Learning Outcomes

  • • Use Iterator::partition(pred) to split into two collections in one pass
  • • Implement three-way partitioning using .fold() for multi-class classification
  • • Use Either<L, R> to route elements with transformation during partitioning
  • • Understand why partition is preferable to two .filter() calls
  • • Compare with OCaml's List.partition and three-way classification idioms
  • Code Example

    pub fn split_even_odd(data: &[i32]) -> (Vec<i32>, Vec<i32>) {
        data.iter().partition(|&&x| x % 2 == 0)
    }

    Key Differences

  • Output order: Rust partition preserves input order; OCaml List.partition using cons-prepend requires List.rev to restore order.
  • Two types: Rust's partition_map with Either can partition into two collections of different types; OCaml partition_map similarly supports this.
  • Three-way: Neither language has built-in three-way partition — both use fold with a triple accumulator.
  • Ownership: Rust's partition on iter() produces Vec<&T>; using into_iter() produces Vec<T> with ownership transfer.
  • OCaml Approach

    List.partition: ('a -> bool) -> 'a list -> 'a list * 'a list is the direct equivalent. OCaml also has List.partition_map: ('a -> ('b, 'c) Either.t) -> 'a list -> 'b list * 'c list (since 4.14). Three-way partition: List.fold_left (fun (a, b, c) x -> if p1 x then (x::a, b, c) else if p2 x then (a, x::b, c) else (a, b, x::c)) ([], [], []) xs. OCaml's lists are singly-linked, so partition is O(n) but the cons-append order reverses the output (requires List.rev).

    Full Source

    #![allow(clippy::all)]
    //! Example 892: Partition Iterator
    //! Split a collection into two groups by a predicate — single pass, two outputs.
    
    // === Approach 1: Basic partition using Iterator::partition ===
    
    /// Split integers into even and odd — idiomatic Rust, one pass.
    pub fn split_even_odd(data: &[i32]) -> (Vec<i32>, Vec<i32>) {
        data.iter().partition(|&&x| x % 2 == 0)
    }
    
    /// Split integers into positive and non-positive.
    pub fn split_positive(data: &[i32]) -> (Vec<i32>, Vec<i32>) {
        data.iter().partition(|&&x| x > 0)
    }
    
    // === Approach 2: Multi-way partition (fold-based, mirrors OCaml's fold_right) ===
    
    /// Partition into three groups by two predicates.
    /// Elements matching `p1` go to `a`, those matching `p2` (but not `p1`) to `b`,
    /// and the rest to `c`.
    pub fn partition3<T: Clone>(
        data: &[T],
        p1: impl Fn(&T) -> bool,
        p2: impl Fn(&T) -> bool,
    ) -> (Vec<T>, Vec<T>, Vec<T>) {
        data.iter().cloned().fold(
            (Vec::new(), Vec::new(), Vec::new()),
            |(mut a, mut b, mut c), x| {
                if p1(&x) {
                    a.push(x);
                } else if p2(&x) {
                    b.push(x);
                } else {
                    c.push(x);
                }
                (a, b, c)
            },
        )
    }
    
    /// Classify integers into negatives, zeros, and positives.
    pub fn classify_numbers(data: &[i32]) -> (Vec<i32>, Vec<i32>, Vec<i32>) {
        partition3(data, |&x| x < 0, |&x| x == 0)
    }
    
    // === Approach 3: partition_map — route elements to Left or Right with transformation ===
    
    pub enum Either<L, R> {
        Left(L),
        Right(R),
    }
    
    /// Partition with simultaneous transformation.
    /// Each element is mapped to `Either::Left` or `Either::Right`; results are collected
    /// into two separate `Vec`s — mirrors OCaml's `Either`-based `partition_map`.
    pub fn partition_map<T, L, R>(data: &[T], f: impl Fn(&T) -> Either<L, R>) -> (Vec<L>, Vec<R>) {
        data.iter()
            .fold((Vec::new(), Vec::new()), |(mut lefts, mut rights), x| {
                match f(x) {
                    Either::Left(l) => lefts.push(l),
                    Either::Right(r) => rights.push(r),
                }
                (lefts, rights)
            })
    }
    
    /// Parse strings into successes (i32) and failures (original string).
    pub fn parse_numbers<'a>(data: &[&'a str]) -> (Vec<i32>, Vec<&'a str>) {
        partition_map(data, |s| match s.parse::<i32>() {
            Ok(n) => Either::Left(n),
            Err(_) => Either::Right(*s),
        })
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- split_even_odd ---
    
        #[test]
        fn test_split_even_odd_empty() {
            let (evens, odds) = split_even_odd(&[]);
            assert_eq!(evens, Vec::<i32>::new());
            assert_eq!(odds, Vec::<i32>::new());
        }
    
        #[test]
        fn test_split_even_odd_mixed() {
            let (evens, odds) = split_even_odd(&[1, 2, 3, 4, 5, 6]);
            assert_eq!(evens, vec![2, 4, 6]);
            assert_eq!(odds, vec![1, 3, 5]);
        }
    
        #[test]
        fn test_split_even_odd_all_even() {
            let (evens, odds) = split_even_odd(&[2, 4, 8]);
            assert_eq!(evens, vec![2, 4, 8]);
            assert!(odds.is_empty());
        }
    
        #[test]
        fn test_split_even_odd_negatives() {
            let (evens, odds) = split_even_odd(&[-4, -3, 0, 1]);
            assert_eq!(evens, vec![-4, 0]);
            assert_eq!(odds, vec![-3, 1]);
        }
    
        // --- split_positive ---
    
        #[test]
        fn test_split_positive_mixed() {
            let (pos, non_pos) = split_positive(&[-2, -1, 0, 1, 2]);
            assert_eq!(pos, vec![1, 2]);
            assert_eq!(non_pos, vec![-2, -1, 0]);
        }
    
        // --- classify_numbers ---
    
        #[test]
        fn test_classify_numbers() {
            let (neg, zero, pos) = classify_numbers(&[-3, -1, 0, 0, 2, 5]);
            assert_eq!(neg, vec![-3, -1]);
            assert_eq!(zero, vec![0, 0]);
            assert_eq!(pos, vec![2, 5]);
        }
    
        #[test]
        fn test_classify_numbers_empty() {
            let (neg, zero, pos) = classify_numbers(&[]);
            assert!(neg.is_empty());
            assert!(zero.is_empty());
            assert!(pos.is_empty());
        }
    
        // --- parse_numbers (partition_map) ---
    
        #[test]
        fn test_parse_numbers_mixed() {
            let (parsed, failed) = parse_numbers(&["1", "two", "3", "four", "5"]);
            assert_eq!(parsed, vec![1, 3, 5]);
            assert_eq!(failed, vec!["two", "four"]);
        }
    
        #[test]
        fn test_parse_numbers_all_valid() {
            let (parsed, failed) = parse_numbers(&["10", "20", "30"]);
            assert_eq!(parsed, vec![10, 20, 30]);
            assert!(failed.is_empty());
        }
    
        #[test]
        fn test_parse_numbers_all_invalid() {
            let (parsed, failed) = parse_numbers(&["a", "b", "c"]);
            assert!(parsed.is_empty());
            assert_eq!(failed, vec!["a", "b", "c"]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- split_even_odd ---
    
        #[test]
        fn test_split_even_odd_empty() {
            let (evens, odds) = split_even_odd(&[]);
            assert_eq!(evens, Vec::<i32>::new());
            assert_eq!(odds, Vec::<i32>::new());
        }
    
        #[test]
        fn test_split_even_odd_mixed() {
            let (evens, odds) = split_even_odd(&[1, 2, 3, 4, 5, 6]);
            assert_eq!(evens, vec![2, 4, 6]);
            assert_eq!(odds, vec![1, 3, 5]);
        }
    
        #[test]
        fn test_split_even_odd_all_even() {
            let (evens, odds) = split_even_odd(&[2, 4, 8]);
            assert_eq!(evens, vec![2, 4, 8]);
            assert!(odds.is_empty());
        }
    
        #[test]
        fn test_split_even_odd_negatives() {
            let (evens, odds) = split_even_odd(&[-4, -3, 0, 1]);
            assert_eq!(evens, vec![-4, 0]);
            assert_eq!(odds, vec![-3, 1]);
        }
    
        // --- split_positive ---
    
        #[test]
        fn test_split_positive_mixed() {
            let (pos, non_pos) = split_positive(&[-2, -1, 0, 1, 2]);
            assert_eq!(pos, vec![1, 2]);
            assert_eq!(non_pos, vec![-2, -1, 0]);
        }
    
        // --- classify_numbers ---
    
        #[test]
        fn test_classify_numbers() {
            let (neg, zero, pos) = classify_numbers(&[-3, -1, 0, 0, 2, 5]);
            assert_eq!(neg, vec![-3, -1]);
            assert_eq!(zero, vec![0, 0]);
            assert_eq!(pos, vec![2, 5]);
        }
    
        #[test]
        fn test_classify_numbers_empty() {
            let (neg, zero, pos) = classify_numbers(&[]);
            assert!(neg.is_empty());
            assert!(zero.is_empty());
            assert!(pos.is_empty());
        }
    
        // --- parse_numbers (partition_map) ---
    
        #[test]
        fn test_parse_numbers_mixed() {
            let (parsed, failed) = parse_numbers(&["1", "two", "3", "four", "5"]);
            assert_eq!(parsed, vec![1, 3, 5]);
            assert_eq!(failed, vec!["two", "four"]);
        }
    
        #[test]
        fn test_parse_numbers_all_valid() {
            let (parsed, failed) = parse_numbers(&["10", "20", "30"]);
            assert_eq!(parsed, vec![10, 20, 30]);
            assert!(failed.is_empty());
        }
    
        #[test]
        fn test_parse_numbers_all_invalid() {
            let (parsed, failed) = parse_numbers(&["a", "b", "c"]);
            assert!(parsed.is_empty());
            assert_eq!(failed, vec!["a", "b", "c"]);
        }
    }

    Deep Comparison

    OCaml vs Rust: Partition Iterator

    Side-by-Side Code

    OCaml

    (* Binary partition: evens vs odds *)
    let split_even_odd lst =
      List.partition (fun x -> x mod 2 = 0) lst
    
    (* Multi-way partition via fold_right *)
    let partition3 p1 p2 lst =
      List.fold_right (fun x (a, b, c) ->
        if p1 x then (x :: a, b, c)
        else if p2 x then (a, x :: b, c)
        else (a, b, x :: c)
      ) lst ([], [], [])
    
    (* Partition with transformation using Either *)
    let partition_map f lst =
      List.fold_right (fun x (lefts, rights) ->
        match f x with
        | Either.Left l  -> (l :: lefts, rights)
        | Either.Right r -> (lefts, r :: rights)
      ) lst ([], [])
    

    Rust (idiomatic — Iterator::partition)

    pub fn split_even_odd(data: &[i32]) -> (Vec<i32>, Vec<i32>) {
        data.iter().partition(|&&x| x % 2 == 0)
    }
    

    Rust (functional/recursive — fold-based multi-way)

    pub fn partition3<T: Clone>(
        data: &[T],
        p1: impl Fn(&T) -> bool,
        p2: impl Fn(&T) -> bool,
    ) -> (Vec<T>, Vec<T>, Vec<T>) {
        data.iter().cloned().fold(
            (Vec::new(), Vec::new(), Vec::new()),
            |(mut a, mut b, mut c), x| {
                if p1(&x) { a.push(x); }
                else if p2(&x) { b.push(x); }
                else { c.push(x); }
                (a, b, c)
            },
        )
    }
    

    Rust (partition_map with user-defined Either)

    pub fn partition_map<T, L, R>(
        data: &[T],
        f: impl Fn(&T) -> Either<L, R>,
    ) -> (Vec<L>, Vec<R>) {
        data.iter().fold((Vec::new(), Vec::new()), |(mut lefts, mut rights), x| {
            match f(x) {
                Either::Left(l)  => lefts.push(l),
                Either::Right(r) => rights.push(r),
            }
            (lefts, rights)
        })
    }
    

    Type Signatures

    ConceptOCamlRust
    Binary partitionval partition : ('a -> bool) -> 'a list -> 'a list * 'a listfn partition(self, f: impl Fn(&T) -> bool) -> (B, B)
    Input collection'a list&[T] (borrowed slice)
    Output collection'a list * 'a list(Vec<T>, Vec<T>)
    Multi-waytuple of 3 lists(Vec<T>, Vec<T>, Vec<T>)
    Either typeEither.Left \| Either.Right (stdlib)user-defined enum Either<L, R>

    Key Insights

  • Single-pass guarantee: Both OCaml's List.partition and Rust's Iterator::partition traverse the input exactly once, routing each element to one of two output collections simultaneously — unlike two separate filter calls.
  • Ownership vs. sharing: OCaml lists share structure freely (persistent data); Rust must either clone (T: Clone) or collect references (Vec<&T>). Cloning here is semantically correct since both output Vecs own their elements independently.
  • **Either availability**: OCaml 4.12+ ships Either in the standard library. Rust's standard library does not expose Either, so partition_map requires a user-defined enum — a common pattern also addressed by the either crate.
  • Multi-way extension: OCaml's fold_right mirrors Rust's .fold(). Both accumulate a tuple of collections in a single left-to-right (or right-to-left in OCaml) scan, making the pattern directly translatable across the two languages.
  • Generic bounds: Rust's Iterator::partition is constrained to B: Default + Extend<Self::Item>, which Vec<T> satisfies out of the box. This genericity means partition works on any collection implementing those traits, not just Vec.
  • When to Use Each Style

    **Use Iterator::partition when:** splitting into exactly two groups with a simple boolean predicate and no transformation needed — it is the most concise and idiomatic choice.

    **Use fold-based multi-way partition when:** you need three or more output buckets, or when the routing logic is complex enough that match branches are clearer than nested if/else.

    **Use partition_map when:** each element must be transformed as it is routed — for example, parsing results where successes become i32 and failures keep the original &str.

    Exercises

  • Implement partition_at_index<T: Clone>(data: &[T], n: usize) -> (Vec<T>, Vec<T>) that splits at position n.
  • Write classify_words(words: &[&str]) -> (Vec<&str>, Vec<&str>, Vec<&str>) that separates short (<5), medium (5-8), and long (>8) words.
  • Implement stable_partition<T: Clone>(data: &[T], pred: impl Fn(&T) -> bool) -> Vec<T> that moves all matching elements to the front while preserving relative order.
  • Open Source Repos