๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Accumulator styleRecursive with `current` + `acc` argsImperative loop with `Vec::push`
List prepend`x :: acc` then `List.rev``Vec::push` (append, no reversal needed)
BorrowingNot distinguished`&[T]` input, `T: Clone` to own the output
Slice referencesNo equivalent`Vec<&[T]>` gives zero-copy grouping
Memory layoutChain of cons cellsContiguous 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

AspectOCamlRust
AccumulatorTwo: `current` + `acc`One: `Vec<Vec<T>>` with `last_mut()`
List buildingPrepend + reverseAppend (amortized O(1))
Zero-copyNot possible`pack_slices` returns `&[T]`
Pattern matching`h1 :: (h2 :: _ as t)`Iterator + conditional
Empty handlingPattern 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