009: Pack Consecutive Duplicates
Difficulty: 2 Level: Beginner Group consecutive equal elements of a list into sublists.The Problem This Solves
Raw sequences often contain repeated values in runs: log levels, sensor readings, characters in a string. Before you can do useful work โ count runs, compress data, summarize patterns โ you need to group consecutive duplicates. Without a dedicated grouping function, you end up writing the same `if current == previous` loop everywhere, tracking a mutable "current group" variable. The logic is correct but verbose, and easy to mess up at the boundary when you flush the last group. `pack` encapsulates the boundary handling: start a group with the first element, extend it when the next element matches, push it and start fresh when the next element differs. The result is a `Vec<Vec<T>>` where each inner vector contains one run.The Intuition
Imagine reading a sequence of colored cards one at a time. You put each card on a pile while the color matches. The moment the color changes, you set the pile aside and start a new one. At the end, you collect all the piles. That's `pack`. The "pile" is `current`, the "set aside" is `result.push(current)`. No look-ahead needed. No sorting. The algorithm is one pass, left to right.How It Works in Rust
fn pack<T: PartialEq + Clone>(list: &[T]) -> Vec<Vec<T>> {
if list.is_empty() { return vec![]; }
let mut result = vec![];
let mut current = vec![list[0].clone()]; // start first group
for i in 1..list.len() {
if list[i] == list[i - 1] {
current.push(list[i].clone()); // same as previous: extend group
} else {
result.push(current); // different: commit group
current = vec![list[i].clone()]; // start fresh group
}
}
result.push(current); // don't forget the last group
result
}
`T: PartialEq` for comparison, `T: Clone` to copy values from the borrowed slice into owned `Vec`s. The key insight is the boundary: you must `push(current)` after the loop to capture the last run.
What This Unlocks
- Run-length encoding โ `pack` is the grouping step; counting each inner `Vec`'s length gives you the run lengths (see example 094).
- Compression algorithms โ many string compression schemes group repeated elements first.
- Change detection โ the "flush on change" pattern works for any sequence where you care about transitions, not just duplicates.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Accumulator style | Recursive with `current` + `acc` args | Imperative loop with `Vec::push` |
| List prepend | `x :: acc` then `List.rev` | `Vec::push` (append, no reversal needed) |
| Borrowing | Not distinguished | `&[T]` input, `T: Clone` to own the output |
| Slice references | No equivalent | `Vec<&[T]>` gives zero-copy grouping |
| Memory layout | Chain of cons cells | Contiguous blocks (`Vec<Vec<T>>`) |
// Pack Consecutive
// Rust translation from OCaml 99 Problems #9
// Pack consecutive duplicates
fn pack<T: PartialEq + Clone>(list: &[T]) -> Vec<Vec<T>> {
if list.is_empty() {
return vec![];
}
let mut result = vec![];
let mut current = vec![list[0].clone()];
for i in 1..list.len() {
if list[i] == list[i - 1] {
current.push(list[i].clone());
} else {
result.push(current);
current = vec![list[i].clone()];
}
}
result.push(current);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pack() {
assert_eq!(
pack(&["a","a","b","c","c","c"]),
vec![vec!["a","a"], vec!["b"], vec!["c","c","c"]]
);
assert_eq!(pack::<i32>(&[]), Vec::<Vec<i32>>::new());
assert_eq!(pack(&[1]), vec![vec![1]]);
}
}
fn main() {
let list = vec!["a","a","b","c","c","c"];
println!("pack({:?}) = {:?}", list, pack(&list));
println!("โ Rust tests passed");
}
(* Pack Consecutive *)
(* OCaml 99 Problems #9 *)
(* Pack consecutive duplicates *)
let pack lst =
let rec aux current acc = function
| [] -> []
| [x] -> (x :: current) :: acc
| h1 :: (h2 :: _ as t) ->
if h1 = h2 then aux (h1 :: current) acc t
else aux [] ((h1 :: current) :: acc) t
in
List.rev (aux [] [] lst)
(* Tests *)
let () =
assert (pack ["a";"a";"b";"c";"c";"c"] = [["a";"a"];["b"];["c";"c";"c"]]);
assert (pack [] = []);
assert (pack [1] = [[1]]);
print_endline "โ OCaml tests passed"
๐ Detailed Comparison
Comparison: Pack Consecutive Duplicates
OCaml โ Accumulator recursion
๐ช Show OCaml equivalent
let pack lst =
let rec aux current acc = function
| [] -> []
| [x] -> (x :: current) :: acc
| h1 :: (h2 :: _ as t) ->
if h1 = h2 then aux (h1 :: current) acc t
else aux [] ((h1 :: current) :: acc) t
in
List.rev (aux [] [] lst)
Rust โ Idiomatic (imperative)
pub fn pack<T: PartialEq + Clone>(list: &[T]) -> Vec<Vec<T>> {
if list.is_empty() { return vec![]; }
let mut result = Vec::new();
let mut current = vec![list[0].clone()];
for item in &list[1..] {
if *item == current[0] {
current.push(item.clone());
} else {
result.push(current);
current = vec![item.clone()];
}
}
result.push(current);
result
}Rust โ Functional (fold)
pub fn pack_fold<T: PartialEq + Clone>(list: &[T]) -> Vec<Vec<T>> {
list.iter().fold(Vec::new(), |mut acc, item| {
match acc.last_mut() {
Some(group) if group[0] == *item => group.push(item.clone()),
_ => acc.push(vec![item.clone()]),
}
acc
})
}Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Accumulator | Two: `current` + `acc` | One: `Vec<Vec<T>>` with `last_mut()` |
| List building | Prepend + reverse | Append (amortized O(1)) |
| Zero-copy | Not possible | `pack_slices` returns `&[T]` |
| Pattern matching | `h1 :: (h2 :: _ as t)` | Iterator + conditional |
| Empty handling | Pattern match `[]` | Early return on `is_empty()` |
Takeaways
1. Rust's `last_mut()` enables elegant fold-based grouping without OCaml's double-accumulator pattern 2. The slice-based version (`pack_slices`) showcases Rust's borrowing โ zero-copy grouping 3. `Vec::push` appending eliminates the `List.rev` step needed in OCaml 4. Both languages use the same core algorithm โ iterate and group โ but express it differently 5. The ownership choice (clone vs borrow) is explicit in Rust, invisible in OCaml