290: Advanced Splitting Patterns
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
unzip() (split pairs by position) from partition() (split by predicate)fold()partition_map patterns (via fold) for splitting while transformingfold branchCode 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
x::n creates a new cons cell; Rust's fold with Vec::push mutates in place.push preserves insertion order.itertools crate's partition_map and partition_fold provide cleaner APIs for common multi-way patterns.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']);
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Partition | List.partition | .partition() |
| Unzip | List.split | .unzip() |
| N-way split | Nested partition or fold | fold with tuple accumulator |
| Single-pass | Depends on laziness | Guaranteed |
| Type hint | Inferred | Often needed for collect |
Exercises
i32, filters evens, and collects both valid evens and parse errors in a single fold.