Eliminate Consecutive Duplicates
Tutorial Video
Text description (accessibility)
This video demonstrates the "Eliminate Consecutive Duplicates" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Lists, Higher-Order Functions. Eliminate consecutive duplicate elements from a list. Key difference from OCaml: 1. **Mutation is explicit**: `dedup()` requires `&mut Vec<T>` — you can't accidentally mutate in Rust
Tutorial
The Problem
Eliminate consecutive duplicate elements from a list. Only remove duplicates that are adjacent — non-adjacent duplicates remain.
Removing consecutive duplicates is the decompression step for run-length encoding. It also appears in log deduplication (suppress duplicate consecutive log messages), text compression preprocessing, and clean-up of sensor data with stuck values. The key constraint — only consecutive duplicates are removed — makes this fundamentally different from removing all duplicates (which requires a HashSet). Run-length decoding produces this exact output.
🎯 Learning Outcomes
dedup() for in-place mutation vs building a new collectionwindows(2) for pairwise element comparison🦀 The Rust Way
Vec::dedup() — in-place, O(n), modifies the vector directlylast() elementwindows(2) to compare pairs, filter where different, collectCode Example
pub fn compress_mut<T: PartialEq>(list: &mut Vec<T>) {
list.dedup();
}Key Differences
dedup() requires &mut Vec<T> — you can't accidentally mutate in Rustwindows(2) is unique to Rust**: Efficient pairwise comparison over contiguous memoryPartialEq explicitly; OCaml uses polymorphic equalitydedup() mutates in place. OCaml lists are immutable — compress always builds a new list.PartialEq vs structural equality:** Rust's dedup uses PartialEq for comparison. OCaml uses structural equality (=) by default, which works for most types.windows(2):** Rust's sliding-window iterator has no built-in OCaml equivalent — it's possible only on contiguous memory (slices/arrays), not linked lists.OCaml Approach
Pattern matches on the list head, comparing consecutive elements. When h1 = h2, skips the duplicate and recurses on the tail. Builds a new list (old one is GC'd).
Full Source
#![allow(clippy::all)]
//! # Eliminate Consecutive Duplicates
//! OCaml 99 Problems #8 — Remove consecutive duplicate elements from a list.
/// Idiomatic Rust: use `dedup` on a mutable Vec (in-place, O(n)).
/// This modifies the vector — Rust's ownership model makes mutation explicit.
pub fn compress_mut<T: PartialEq>(list: &mut Vec<T>) {
list.dedup();
}
/// Functional style: build a new Vec by filtering consecutive duplicates.
/// Mirrors the OCaml recursive approach but uses iterators.
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
}
/// Iterator-based with windows — the most declarative approach.
/// Uses `windows(2)` to compare adjacent pairs.
pub fn compress_iter<T: PartialEq + Clone>(list: &[T]) -> Vec<T> {
if list.is_empty() {
return vec![];
}
let mut result: Vec<T> = list
.windows(2)
.filter(|w| w[0] != w[1])
.map(|w| w[0].clone())
.collect();
// Always include the last element
if let Some(last) = list.last() {
result.push(last.clone());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_consecutive_duplicates() {
let input = vec!["a", "a", "a", "b", "c", "c", "d", "e", "e", "e"];
assert_eq!(compress(&input), vec!["a", "b", "c", "d", "e"]);
assert_eq!(compress_iter(&input), vec!["a", "b", "c", "d", "e"]);
}
#[test]
fn test_empty() {
assert_eq!(compress::<i32>(&[]), Vec::<i32>::new());
assert_eq!(compress_iter::<i32>(&[]), Vec::<i32>::new());
}
#[test]
fn test_single() {
assert_eq!(compress(&[1]), vec![1]);
assert_eq!(compress_iter(&[1]), vec![1]);
}
#[test]
fn test_no_duplicates() {
assert_eq!(compress(&[1, 2, 3, 4]), vec![1, 2, 3, 4]);
}
#[test]
fn test_all_same() {
assert_eq!(compress(&[5, 5, 5, 5]), vec![5]);
}
#[test]
fn test_mut_version() {
let mut v = vec![1, 1, 2, 2, 3];
compress_mut(&mut v);
assert_eq!(v, vec![1, 2, 3]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_consecutive_duplicates() {
let input = vec!["a", "a", "a", "b", "c", "c", "d", "e", "e", "e"];
assert_eq!(compress(&input), vec!["a", "b", "c", "d", "e"]);
assert_eq!(compress_iter(&input), vec!["a", "b", "c", "d", "e"]);
}
#[test]
fn test_empty() {
assert_eq!(compress::<i32>(&[]), Vec::<i32>::new());
assert_eq!(compress_iter::<i32>(&[]), Vec::<i32>::new());
}
#[test]
fn test_single() {
assert_eq!(compress(&[1]), vec![1]);
assert_eq!(compress_iter(&[1]), vec![1]);
}
#[test]
fn test_no_duplicates() {
assert_eq!(compress(&[1, 2, 3, 4]), vec![1, 2, 3, 4]);
}
#[test]
fn test_all_same() {
assert_eq!(compress(&[5, 5, 5, 5]), vec![5]);
}
#[test]
fn test_mut_version() {
let mut v = vec![1, 1, 2, 2, 3];
compress_mut(&mut v);
assert_eq!(v, vec![1, 2, 3]);
}
}
Deep Comparison
Comparison: Eliminate Consecutive Duplicates
OCaml — Pattern matching
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
| Aspect | OCaml | Rust |
|---|---|---|
| Pattern | h1 :: h2 :: _ (cons cell) | &list[1..] (slice) |
| Mutation | Not available on lists | Vec::dedup() in-place |
| Return | New list (GC frees old) | New Vec or mutate existing |
| Equality | = (polymorphic) | PartialEq trait |
| Edge cases | Pattern match [], [x] | is_empty() check |
Takeaways
dedup() is a one-liner for the imperative approach — stdlib is batteries-includedwindows(2) approach has no OCaml equivalent — it leverages contiguous memory layout&mut), never accidentalExercises
deduplicate_all (not just consecutive) — remove every duplicate from a list, keeping only the first occurrence of each element.deduplicate_by that removes consecutive duplicates using a key function f: &T -> K for comparison, so you can deduplicate case-insensitively or by a struct field.run_length_from_dedup that uses eliminate_consecutive as a building block to produce run-length encoding in a single pipeline (no extra passes).