๐Ÿฆ€ Functional Rust

008: Eliminate Consecutive Duplicates

Difficulty: โญ Level: Beginner Remove repeated adjacent elements from a list, keeping one of each run.

The Problem This Solves

You have a sequence with repeated runs: `["a", "a", "a", "b", "c", "c", "d", "e", "e"]`. You want: `["a", "b", "c", "d", "e"]`. This comes up in log deduplication, compressing repeated sensor readings, processing run-length encoded data, or collapsing repeated keypresses. Note the "consecutive" qualifier: only adjacent duplicates are removed. `["a", "b", "a"]` stays as-is โ€” the two `"a"`s are not adjacent. If you want to remove all duplicates regardless of position, that's a different problem (use a `HashSet`). The loop-based approach is straightforward but wordy: track the "last seen" element, compare, conditionally push. Rust offers two cleaner paths: a one-liner for in-place mutation, and a functional version for when you need a new collection.

The Intuition

# Python โ€” no built-in, need itertools or a loop
from itertools import groupby
result = [k for k, _ in groupby(lst)]

# JavaScript โ€” no built-in, manual filter
const result = arr.filter((x, i) => i === 0 || x !== arr[i-1]);
// Rust โ€” in-place, one line
vec.dedup();

// Or functional โ€” returns a new Vec
let result: Vec<_> = list.iter()
 .enumerate()
 .filter(|(i, x)| *i == 0 || list[i-1] != **x)
 .map(|(_, x)| x.clone())
 .collect();
`dedup()` is short for "deduplicate". It's built into `Vec<T>` and runs in a single pass, O(n) time, O(1) extra space. It requires `T: PartialEq` โ€” the elements must be comparable.

How It Works in Rust

// Option 1: In-place mutation (fastest, no allocation)
fn compress(list: &mut Vec<T>) where T: PartialEq {
 list.dedup();  // modifies the Vec directly
}

// Option 2: Functional โ€” returns a new Vec (input unchanged)
fn compress_functional<T: PartialEq + Clone>(list: &[T]) -> Vec<T> {
 list.iter()
     .enumerate()
     .filter(|(i, x)| *i == 0 || list[i - 1] != **x)
     .map(|(_, x)| x.clone())
     .collect()
}
The `&mut` in Option 1 is explicit โ€” you're saying "I'm modifying this data." In JavaScript, mutation is implicit and often surprising. In Rust, you can't mutate without opt-in. The `windows(2)` approach compares adjacent pairs directly:
// Using windows โ€” reads naturally as "pairs of neighbors"
fn compress_windows<T: PartialEq + Clone>(list: &[T]) -> Vec<T> {
 if list.is_empty() { return vec![]; }
 
 let mut result = vec![list[0].clone()];
 result.extend(
     list.windows(2)
         .filter(|w| w[0] != w[1])
         .map(|w| w[1].clone())
 );
 result
}
`windows(2)` gives you overlapping pairs: `[a,b]`, `[b,c]`, `[c,d]`... wherever two neighbors differ, the second one belongs in the result.

What This Unlocks

Key Differences

ConceptOCamlRust
Idiomatic approachRecursive pattern match on cons cells`vec.dedup()` (in-place)
In-place optionNot available (immutable lists)`dedup()` with `&mut Vec<T>`
Pairwise comparison`h1 :: h2 :: _` pattern`windows(2)` iterator
EqualityPolymorphic `=``PartialEq` trait bound
New list vs mutationAlways new listExplicit choice (`&` vs `&mut`)
// Eliminate Duplicates
// Rust translation from OCaml 99 Problems #8

// Idiomatic Rust (in-place)
fn compress<T: PartialEq>(list: &mut Vec<T>) {
    list.dedup();
}

// Functional style (returns new vec)
fn compress_functional<T: PartialEq + Clone>(list: &[T]) -> Vec<T> {
    list.iter()
        .enumerate()
        .filter(|(i, x)| *i == 0 || list[i - 1] != **x)
        .map(|(_, x)| x.clone())
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_compress() {
        assert_eq!(
            compress_functional(&["a","a","a","b","c","c","d","e","e","e"]),
            vec!["a","b","c","d","e"]
        );
        assert_eq!(compress_functional::<i32>(&[]), Vec::<i32>::new());
        assert_eq!(compress_functional(&[1]), vec![1]);
    }
}

fn main() {
    let list = vec!["a","a","a","b","c","c"];
    println!("compress({:?}) = {:?}", list, compress_functional(&list));
    println!("โœ“ Rust tests passed");
}
(* Eliminate Duplicates *)
(* OCaml 99 Problems #8 *)

(* Eliminate consecutive duplicates *)
let rec compress = function
  | [] -> []
  | [x] -> [x]
  | h1 :: (h2 :: _ as t) ->
      if h1 = h2 then compress t
      else h1 :: compress t

(* Tests *)
let () =
  assert (compress ["a";"a";"a";"b";"c";"c";"d";"e";"e";"e"] = ["a";"b";"c";"d";"e"]);
  assert (compress [] = []);
  assert (compress [1] = [1]);
  print_endline "โœ“ OCaml tests passed"

๐Ÿ“Š Detailed Comparison

Comparison: Eliminate Consecutive Duplicates

OCaml โ€” Pattern matching

๐Ÿช Show OCaml equivalent
let rec compress = function
| [] -> []
| [x] -> [x]
| h1 :: (h2 :: _ as t) ->
   if h1 = h2 then compress t
   else h1 :: compress t

Rust โ€” Idiomatic (in-place mutation)

pub fn compress_mut<T: PartialEq>(list: &mut Vec<T>) {
 list.dedup();
}

Rust โ€” Functional (accumulator)

pub fn compress<T: PartialEq + Clone>(list: &[T]) -> Vec<T> {
 if list.is_empty() { return vec![]; }
 let mut result = vec![list[0].clone()];
 for item in &list[1..] {
     if result.last() != Some(item) {
         result.push(item.clone());
     }
 }
 result
}

Comparison Table

AspectOCamlRust
Pattern`h1 :: h2 :: _` (cons cell)`&list[1..]` (slice)
MutationNot available on lists`Vec::dedup()` in-place
ReturnNew list (GC frees old)New `Vec` or mutate existing
Equality`=` (polymorphic)`PartialEq` trait
Edge casesPattern match `[]`, `[x]``is_empty()` check

Takeaways

1. Rust's `dedup()` is a one-liner for the imperative approach โ€” stdlib is batteries-included 2. OCaml's pattern matching on cons cells is more elegant for list algorithms 3. The `windows(2)` approach has no OCaml equivalent โ€” it leverages contiguous memory layout 4. Mutation in Rust is always a conscious choice (`&mut`), never accidental 5. Both languages handle edge cases (empty, single-element) but Rust uses conditionals where OCaml uses pattern arms