ExamplesBy LevelBy TopicLearning Paths
1125 Intermediate

List.partition — Split by Predicate

Functional Programming

Tutorial

The Problem

Split a collection into two parts in a single traversal: elements that satisfy a predicate go into one group, and those that do not go into another. The function returns both groups simultaneously as a tuple, making it more efficient and expressive than two separate filter operations. This example implements a generic partition function and derives concrete domain helpers — splitting integers by a numeric threshold and strings by length — to show how the abstraction applies in practice.

🎯 Learning Outcomes

  • • How OCaml's List.partition pred xs maps to Rust's Iterator::partition, and why both return a two-element tuple rather than requiring two passes
  • • How to return multiple collections simultaneously using tuple destructuring in both languages — and how nearly identical the destructuring syntax is across the two
  • • Why a single-pass partition is O(n) with constant overhead, while calling filter twice is also O(n) but doubles the work and requires the predicate to be called twice per element
  • • How to write a generic wrapper over Rust's iterator adapter, including the Clone bound needed to go from &[T] to owned Vec<T> output
  • • The difference between Rust's Fn(&T) -> bool predicate bound and OCaml's structurally-typed anonymous functions — and why Rust requires the bound to be spelled out
  • Code Example

    pub fn partition<T: Clone, F>(items: &[T], pred: F) -> (Vec<T>, Vec<T>)
    where
        F: Fn(&T) -> bool,
    {
        items.iter().cloned().partition(|x| pred(x))
    }

    Key Differences

  • Single pass vs. two filters: Both List.partition and Iterator::partition traverse the collection once; two separate filter calls would traverse it twice and call the predicate on every element twice — important when the predicate is expensive or the collection is large.
  • Tuple destructuring syntax: OCaml: let (small, big) = List.partition ...; Rust: let (small, big) = partition(...) — the syntax is identical, which is a pleasant surprise when coming from OCaml.
  • Predicate type: OCaml accepts any function 'a -> bool through structural typing; Rust requires the generic bound F: Fn(&T) -> bool, which must be declared explicitly in the function signature and is enforced by the compiler.
  • Ownership and cloning: OCaml lists share structure safely through garbage collection; Rust requires T: Clone to move values from a borrowed slice into two owned Vecs, making the allocation cost visible in the type signature.
  • OCaml Approach

    OCaml's List.partition accepts a predicate and a list, and returns a pair (matches, non_matches) where both lists maintain the original relative order of elements. Internally it is implemented as a single List.fold_left, accumulating into two separate accumulators. The result is destructured with let (small, big) = List.partition (fun x -> x <= 5) numbers — pattern matching on the pair directly in the let binding. The predicate is an ordinary first-class function; no special syntax or trait is needed to pass it.

    Full Source

    #![allow(clippy::all)]
    //! List.partition — Split by Predicate
    //! See example.ml for OCaml reference
    //!
    //! OCaml's `List.partition pred xs` returns `(matches, non_matches)` in a single pass.
    //! Rust's `Iterator::partition` does the same, collecting into two separate `Vec`s.
    
    /// Split a slice into two vecs: elements satisfying `pred` and those that don't.
    /// Mirrors OCaml: `List.partition (fun x -> x <= 5) numbers`
    pub fn partition<T: Clone, F>(items: &[T], pred: F) -> (Vec<T>, Vec<T>)
    where
        F: Fn(&T) -> bool,
    {
        items.iter().cloned().partition(|x| pred(x))
    }
    
    /// Partition integers into (small, big) where small means `<= threshold`.
    pub fn partition_threshold(numbers: &[i32], threshold: i32) -> (Vec<i32>, Vec<i32>) {
        partition(numbers, |&x| x <= threshold)
    }
    
    /// Partition strings by length: (short, long) where short means `len <= max_len`.
    pub fn partition_by_length<'a>(words: &[&'a str], max_len: usize) -> (Vec<&'a str>, Vec<&'a str>) {
        partition(words, |s| s.len() <= max_len)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partition_numbers() {
            let numbers: Vec<i32> = (1..=10).collect();
            let (small, big) = partition_threshold(&numbers, 5);
            assert_eq!(small, vec![1, 2, 3, 4, 5]);
            assert_eq!(big, vec![6, 7, 8, 9, 10]);
        }
    
        #[test]
        fn test_partition_empty() {
            let (small, big) = partition_threshold(&[], 5);
            assert!(small.is_empty());
            assert!(big.is_empty());
        }
    
        #[test]
        fn test_partition_all_match() {
            let nums = vec![1i32, 2, 3];
            let (small, big) = partition_threshold(&nums, 10);
            assert_eq!(small, vec![1, 2, 3]);
            assert!(big.is_empty());
        }
    
        #[test]
        fn test_partition_none_match() {
            let nums = vec![6i32, 7, 8];
            let (small, big) = partition_threshold(&nums, 5);
            assert!(small.is_empty());
            assert_eq!(big, vec![6, 7, 8]);
        }
    
        #[test]
        fn test_partition_by_length() {
            let words = vec!["hi", "hello", "ok", "world", "rust"];
            let (short, long) = partition_by_length(&words, 3);
            assert_eq!(short, vec!["hi", "ok"]);
            assert_eq!(long, vec!["hello", "world", "rust"]);
        }
    
        #[test]
        fn test_partition_evens_odds() {
            let nums: Vec<i32> = (1..=6).collect();
            let (evens, odds) = partition(&nums, |&x| x % 2 == 0);
            assert_eq!(evens, vec![2, 4, 6]);
            assert_eq!(odds, vec![1, 3, 5]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partition_numbers() {
            let numbers: Vec<i32> = (1..=10).collect();
            let (small, big) = partition_threshold(&numbers, 5);
            assert_eq!(small, vec![1, 2, 3, 4, 5]);
            assert_eq!(big, vec![6, 7, 8, 9, 10]);
        }
    
        #[test]
        fn test_partition_empty() {
            let (small, big) = partition_threshold(&[], 5);
            assert!(small.is_empty());
            assert!(big.is_empty());
        }
    
        #[test]
        fn test_partition_all_match() {
            let nums = vec![1i32, 2, 3];
            let (small, big) = partition_threshold(&nums, 10);
            assert_eq!(small, vec![1, 2, 3]);
            assert!(big.is_empty());
        }
    
        #[test]
        fn test_partition_none_match() {
            let nums = vec![6i32, 7, 8];
            let (small, big) = partition_threshold(&nums, 5);
            assert!(small.is_empty());
            assert_eq!(big, vec![6, 7, 8]);
        }
    
        #[test]
        fn test_partition_by_length() {
            let words = vec!["hi", "hello", "ok", "world", "rust"];
            let (short, long) = partition_by_length(&words, 3);
            assert_eq!(short, vec!["hi", "ok"]);
            assert_eq!(long, vec!["hello", "world", "rust"]);
        }
    
        #[test]
        fn test_partition_evens_odds() {
            let nums: Vec<i32> = (1..=6).collect();
            let (evens, odds) = partition(&nums, |&x| x % 2 == 0);
            assert_eq!(evens, vec![2, 4, 6]);
            assert_eq!(odds, vec![1, 3, 5]);
        }
    }

    Deep Comparison

    OCaml vs Rust: List.partition — Split by Predicate

    Side-by-Side Code

    OCaml

    let numbers = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
    let (small, big) = List.partition (fun x -> x <= 5) numbers
    

    Rust (idiomatic)

    pub fn partition<T: Clone, F>(items: &[T], pred: F) -> (Vec<T>, Vec<T>)
    where
        F: Fn(&T) -> bool,
    {
        items.iter().cloned().partition(|x| pred(x))
    }
    

    Rust (functional fold)

    // Mirrors OCaml's accumulator-based implementation explicitly.
    pub fn partition_fold<T: Clone, F>(items: &[T], pred: F) -> (Vec<T>, Vec<T>)
    where
        F: Fn(&T) -> bool,
    {
        items.iter().fold((vec![], vec![]), |(mut yes, mut no), x| {
            if pred(x) { yes.push(x.clone()); } else { no.push(x.clone()); }
            (yes, no)
        })
    }
    

    Type Signatures

    ConceptOCamlRust
    partition('a -> bool) -> 'a list -> 'a list * 'a listfn partition<T: Clone, F>(&[T], F) -> (Vec<T>, Vec<T>)
    result type'a list * 'a list (tuple)(Vec<T>, Vec<T>) (tuple)
    predicate'a -> boolF: Fn(&T) -> bool
    destructuringlet (small, big) = ...let (small, big) = ...

    Key Insights

  • Single-pass vs. two-filter: Both OCaml and Rust partition traverse the list once and split into two output collections, which is more efficient than calling filter twice.
  • Iterator adapter: Rust's Iterator::partition is a built-in method that collects into a pair of collections — exactly matching OCaml's List.partition semantics.
  • Tuple destructuring: Both languages support let (a, b) = partition(...) for destructuring the result tuple — the syntax is nearly identical.
  • Clone requirement: Rust must clone() each element since ownership prevents moving items into two separate Vecs simultaneously. OCaml's GC shares list nodes freely.
  • Predicate signature: OCaml's predicate receives a value 'a; Rust's receives a reference &T (since the iterator yields &T from a &[T] slice). The cloned() adapter materializes the T after filtering.
  • When to Use Each Style

    **Use Iterator::partition when:** you need to split a slice into two groups in a single pass — it's the most idiomatic and readable approach. Use the fold-based partition when: you want to accumulate into more complex structures than a pair of Vecs (e.g., a HashMap of groups), or when teaching the fold-based derivation of partition.

    Exercises

  • Implement partition_map that applies a function returning Result<A, B> to each element and collects Ok(a) values into one Vec<A> and Err(b) values into another Vec<B>, in a single pass. This mirrors the OCaml List.partition_map added in OCaml 4.12.
  • Implement a three-way partition: given two thresholds low and high, return (below, middle, above) in a single traversal. Verify that the union of all three groups contains exactly the elements of the input with no duplicates or omissions.
  • Write a property-based test (or a parameterized Vitest-style test over many random inputs) that verifies partition and two separate filter calls always produce the same result, and that the concatenation of both output Vecs is a permutation of the input slice.
  • Open Source Repos