ExamplesBy LevelBy TopicLearning Paths
290 Intermediate

290: Advanced Splitting Patterns

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "290: Advanced Splitting Patterns" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Real-world data classification often requires more than a binary split. Key difference from OCaml: 1. **Immutable accumulation**: OCaml's fold accumulator is passed by value and returned — `x::n` creates a new cons cell; Rust's `fold` with `Vec::push` mutates in place.

Tutorial

The Problem

Real-world data classification often requires more than a binary split. Partitioning numbers into negative, zero, and positive; splitting strings by parse success while keeping both results; routing events to different queues — these require multi-way classification in a single pass. This example explores unzip, partition, and custom fold-based trisection patterns, demonstrating when to use each.

🎯 Learning Outcomes

  • • Distinguish unzip() (split pairs by position) from partition() (split by predicate)
  • • Implement multi-way classification beyond binary using fold()
  • • Use partition_map patterns (via fold) for splitting while transforming
  • • Recognize that each additional split requires one more collection — and one more fold branch
  • Code Example

    let nums = vec![-3, 0, 1, -1, 0, 5];
    let (neg, non_neg): (Vec<i32>, Vec<i32>) = 
        nums.into_iter().partition(|&x| x < 0);

    Key Differences

  • Immutable accumulation: OCaml's fold accumulator is passed by value and returned — x::n creates a new cons cell; Rust's fold with Vec::push mutates in place.
  • Ordering: OCaml's fold-based partition reverses order (prepend to list); Rust's push preserves insertion order.
  • Ergonomics: The itertools crate's partition_map and partition_fold provide cleaner APIs for common multi-way patterns.
  • Real-world use: Router dispatching, event classification, multi-bucket histogram construction.
  • OCaml Approach

    OCaml's List.partition handles binary splits. For multi-way classification, List.fold_left with a tuple accumulator is the standard approach — identical in structure to the Rust fold pattern:

    let (neg, zero, pos) = List.fold_left (fun (n,z,p) x ->
      if x < 0 then (x::n, z, p)
      else if x = 0 then (n, x::z, p)
      else (n, z, x::p)
    ) ([], [], []) nums
    

    Full Source

    #![allow(clippy::all)]
    //! # Advanced Splitting Patterns
    //!
    //! Split iterators into multiple collections in a single pass — unzip, partition, and multi-way categorization.
    
    /// Partition numbers into negative and non-negative
    pub fn partition_by_sign(nums: Vec<i32>) -> (Vec<i32>, Vec<i32>) {
        nums.into_iter().partition(|&x| x < 0)
    }
    
    /// Unzip pairs into two separate collections
    pub fn unzip_pairs<A, B>(pairs: Vec<(A, B)>) -> (Vec<A>, Vec<B>) {
        pairs.into_iter().unzip()
    }
    
    /// Partition map pattern: split by parse success
    pub fn partition_parse(data: &[&str]) -> (Vec<i32>, Vec<String>) {
        data.iter()
            .fold((Vec::new(), Vec::new()), |(mut nums, mut words), s| {
                match s.parse::<i32>() {
                    Ok(n) => nums.push(n),
                    Err(_) => words.push(s.to_string()),
                }
                (nums, words)
            })
    }
    
    /// Trisect: split into negative, zero, positive
    pub fn trisect(nums: Vec<i32>) -> (Vec<i32>, Vec<i32>, Vec<i32>) {
        nums.into_iter().fold(
            (Vec::new(), Vec::new(), Vec::new()),
            |(mut neg, mut zero, mut pos), n| {
                if n < 0 {
                    neg.push(n);
                } else if n == 0 {
                    zero.push(n);
                } else {
                    pos.push(n);
                }
                (neg, zero, pos)
            },
        )
    }
    
    /// Categorize by size
    pub fn categorize_by_size(values: &[u32]) -> (Vec<u32>, Vec<u32>, Vec<u32>) {
        values.iter().fold(
            (Vec::new(), Vec::new(), Vec::new()),
            |(mut small, mut medium, mut large), &v| {
                match v {
                    0..=10 => small.push(v),
                    11..=100 => medium.push(v),
                    _ => large.push(v),
                }
                (small, medium, large)
            },
        )
    }
    
    /// Nested unzip - separate pairs-of-pairs
    pub fn nested_unzip(nested: Vec<((i32, i32), char)>) -> (Vec<i32>, Vec<i32>, Vec<char>) {
        let (pairs, labels): (Vec<(i32, i32)>, Vec<char>) = nested.into_iter().unzip();
        let (lefts, rights): (Vec<i32>, Vec<i32>) = pairs.into_iter().unzip();
        (lefts, rights, labels)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partition_by_sign() {
            let (neg, non_neg) = partition_by_sign(vec![-3, -1, 0, 1, 2, 5]);
            assert_eq!(neg, vec![-3, -1]);
            assert_eq!(non_neg, vec![0, 1, 2, 5]);
        }
    
        #[test]
        fn test_unzip_pairs() {
            let (nums, chars) = unzip_pairs(vec![(1, 'a'), (2, 'b'), (3, 'c')]);
            assert_eq!(nums, vec![1, 2, 3]);
            assert_eq!(chars, vec!['a', 'b', 'c']);
        }
    
        #[test]
        fn test_partition_parse() {
            let (nums, words) = partition_parse(&["1", "two", "3", "four"]);
            assert_eq!(nums, vec![1, 3]);
            assert_eq!(words, vec!["two", "four"]);
        }
    
        #[test]
        fn test_trisect() {
            let (neg, zero, pos) = trisect(vec![-3, 0, 1, -1, 0, 5, -2, 3]);
            assert_eq!(neg, vec![-3, -1, -2]);
            assert_eq!(zero, vec![0, 0]);
            assert_eq!(pos, vec![1, 5, 3]);
        }
    
        #[test]
        fn test_categorize_by_size() {
            let (small, medium, large) = categorize_by_size(&[1, 15, 100, 8, 50, 3, 200]);
            assert_eq!(small, vec![1, 8, 3]);
            assert_eq!(medium, vec![15, 100, 50]);
            assert_eq!(large, vec![200]);
        }
    
        #[test]
        fn test_nested_unzip() {
            let (lefts, rights, labels) = nested_unzip(vec![((1, 2), 'a'), ((3, 4), 'b')]);
            assert_eq!(lefts, vec![1, 3]);
            assert_eq!(rights, vec![2, 4]);
            assert_eq!(labels, vec!['a', 'b']);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_partition_by_sign() {
            let (neg, non_neg) = partition_by_sign(vec![-3, -1, 0, 1, 2, 5]);
            assert_eq!(neg, vec![-3, -1]);
            assert_eq!(non_neg, vec![0, 1, 2, 5]);
        }
    
        #[test]
        fn test_unzip_pairs() {
            let (nums, chars) = unzip_pairs(vec![(1, 'a'), (2, 'b'), (3, 'c')]);
            assert_eq!(nums, vec![1, 2, 3]);
            assert_eq!(chars, vec!['a', 'b', 'c']);
        }
    
        #[test]
        fn test_partition_parse() {
            let (nums, words) = partition_parse(&["1", "two", "3", "four"]);
            assert_eq!(nums, vec![1, 3]);
            assert_eq!(words, vec!["two", "four"]);
        }
    
        #[test]
        fn test_trisect() {
            let (neg, zero, pos) = trisect(vec![-3, 0, 1, -1, 0, 5, -2, 3]);
            assert_eq!(neg, vec![-3, -1, -2]);
            assert_eq!(zero, vec![0, 0]);
            assert_eq!(pos, vec![1, 5, 3]);
        }
    
        #[test]
        fn test_categorize_by_size() {
            let (small, medium, large) = categorize_by_size(&[1, 15, 100, 8, 50, 3, 200]);
            assert_eq!(small, vec![1, 8, 3]);
            assert_eq!(medium, vec![15, 100, 50]);
            assert_eq!(large, vec![200]);
        }
    
        #[test]
        fn test_nested_unzip() {
            let (lefts, rights, labels) = nested_unzip(vec![((1, 2), 'a'), ((3, 4), 'b')]);
            assert_eq!(lefts, vec![1, 3]);
            assert_eq!(rights, vec![2, 4]);
            assert_eq!(labels, vec!['a', 'b']);
        }
    }

    Deep Comparison

    OCaml vs Rust: unzip and partition

    Pattern 1: Partition by Predicate

    OCaml

    let nums = [-3; 0; 1; -1; 0; 5] in
    let (neg, non_neg) = List.partition (fun x -> x < 0) nums
    

    Rust

    let nums = vec![-3, 0, 1, -1, 0, 5];
    let (neg, non_neg): (Vec<i32>, Vec<i32>) = 
        nums.into_iter().partition(|&x| x < 0);
    

    Pattern 2: Unzip Pairs

    OCaml

    let pairs = [(1, 'a'); (2, 'b'); (3, 'c')] in
    let (nums, chars) = List.split pairs
    

    Rust

    let pairs = vec![(1, 'a'), (2, 'b'), (3, 'c')];
    let (nums, chars): (Vec<i32>, Vec<char>) = pairs.into_iter().unzip();
    

    Pattern 3: Multi-way Split with Fold

    OCaml

    type ('a, 'b) either = Left of 'a | Right of 'b
    let partition_map f lst =
      List.fold_left (fun (ls, rs) x ->
        match f x with
        | Left l -> (l :: ls, rs)
        | Right r -> (ls, r :: rs)
      ) ([], []) lst |> fun (ls, rs) -> (List.rev ls, List.rev rs)
    

    Rust

    let (nums, words) = data.iter().fold(
        (Vec::new(), Vec::new()),
        |(mut ns, mut ws), s| {
            match s.parse::<i32>() {
                Ok(n) => ns.push(n),
                Err(_) => ws.push(s),
            }
            (ns, ws)
        }
    );
    

    Key Differences

    AspectOCamlRust
    PartitionList.partition.partition()
    UnzipList.split.unzip()
    N-way splitNested partition or foldfold with tuple accumulator
    Single-passDepends on lazinessGuaranteed
    Type hintInferredOften needed for collect

    Exercises

  • Implement a four-way classifier that sorts strings into "short" (≤3), "medium" (4-7), "long" (8-12), and "very long" (>12) buckets.
  • Use fold to simultaneously partition results into successes and failures while counting each.
  • Build a pipeline that parses strings into i32, filters evens, and collects both valid evens and parse errors in a single fold.
  • Open Source Repos