114: Slice Patterns
Difficulty: 2 Level: Intermediate Pattern match on the shape of slices โ `[first, rest @ ..]`, `[a, b]`, `[head, .., tail]` โ the same structural thinking as OCaml list patterns, but on contiguous memory.The Problem This Solves
In C, processing arrays means index-based loops with manual bounds checks. Want to handle "empty array", "one element", and "two or more elements" differently? Three `if` branches, checking `len == 0`, `len == 1`, `len >= 2`. Miss a case, or check the wrong index, and you have an out-of-bounds access. Most languages with pattern matching focus on ADTs (algebraic data types) and enums. But sequences โ arrays, slices, lists โ are ubiquitous. Having to write structural code with explicit indexing when you could just match on shape is a missed opportunity. OCaml's list patterns (`| [] -> ... | x :: rest -> ...`) are elegant for linked lists but don't apply to arrays. Rust's slice patterns fill this gap: you can match on the structure of any contiguous sequence with the same clarity. The compiler guarantees exhaustiveness โ every case you forgot is a warning or error, not a runtime crash.The Intuition
Slice patterns let you describe the shape of a slice โ "empty", "exactly one element", "first element and the rest" โ and the compiler matches structurally, with exhaustiveness checking to ensure no case is missed.How It Works in Rust
fn describe_slice(nums: &[i32]) -> &str {
match nums {
[] => "empty",
[_] => "exactly one element",
[_, _] => "exactly two elements",
[first, .., last] => {
println!("first={}, last={}", first, last);
"three or more elements"
}
}
}
// Recursive-style processing with slice patterns
fn sum_recursive(nums: &[i32]) -> i32 {
match nums {
[] => 0,
[head, tail @ ..] => head + sum_recursive(tail),
// tail @ .. binds the rest of the slice to `tail`
}
}
// Destructure specific positions
fn first_two(nums: &[i32]) -> Option<(i32, i32)> {
match nums {
[a, b, ..] => Some((*a, *b)),
_ => None,
}
}
// Nested patterns
fn starts_and_ends(data: &[&str]) -> Option<(&str, &str)> {
match data {
[first, .., last] => Some((first, last)),
[only] => Some((only, only)),
[] => None,
}
}
// Matching with guards
fn categorize(nums: &[i32]) -> &str {
match nums {
[] => "empty",
[n] if *n > 0 => "single positive",
[n] if *n < 0 => "single negative",
[n] => "single zero", // n == 0
[a, b] if a == b => "two equal elements",
_ => "general case",
}
}
fn demo() {
println!("{}", describe_slice(&[])); // "empty"
println!("{}", describe_slice(&[42])); // "exactly one element"
println!("{}", describe_slice(&[1,2,3])); // "three or more elements"
println!("{}", sum_recursive(&[1, 2, 3, 4, 5])); // 15
println!("{:?}", first_two(&[10, 20, 30])); // Some((10, 20))
}
What This Unlocks
- Exhaustive sequence matching โ the compiler warns if you miss a case; no silent bugs from forgetting to handle an empty slice.
- OCaml-style structural thinking on arrays โ write recursive algorithms on slices with the same elegance as OCaml's list patterns, but on contiguous memory (no linked-list overhead).
- Readable over-indexing โ `[first, second, ..]` is more readable than `nums[0]` and `nums[1]` with manual length checks.
Key Differences
| Concept | OCaml | Rust | |
|---|---|---|---|
| Sequence pattern matching | `[]` and `x :: rest` on lists | `[]`, `[x]`, `[h, t @ ..]` on slices | |
| Underlying data structure | Linked list (cons cells) | Contiguous memory (slice `&[T]`) | |
| Rest binding | `match lst with \ | x :: rest ->` | `[head, rest @ ..]` |
| Random access cost | O(n) for lists | O(1) for slices | |
| Exhaustiveness | Compiler-checked | Compiler-checked |
// Example 114: Slice Patterns
//
// Rust can pattern match on slices: [first, rest @ ..], [a, b], etc.
// Similar to OCaml's list patterns but on contiguous memory.
// Approach 1: Head/tail with slice patterns
fn sum(slice: &[i32]) -> i32 {
match slice {
[] => 0,
[x, rest @ ..] => x + sum(rest),
}
}
fn take(n: usize, slice: &[i32]) -> Vec<i32> {
match (n, slice) {
(0, _) | (_, []) => vec![],
(n, [x, rest @ ..]) => {
let mut result = vec![*x];
result.extend(take(n - 1, rest));
result
}
}
}
fn approach1() {
assert_eq!(sum(&[1, 2, 3, 4, 5]), 15);
assert_eq!(take(3, &[1, 2, 3, 4, 5]), vec![1, 2, 3]);
println!("Sum: {}, Take 3: {:?}", sum(&[1,2,3,4,5]), take(3, &[1,2,3,4,5]));
}
// Approach 2: Matching specific shapes
fn describe(slice: &[i32]) -> &str {
match slice {
[] => "empty",
[_] => "singleton",
[_, _] => "pair",
[_, _, ..] => "many",
}
}
fn approach2() {
assert_eq!(describe(&[]), "empty");
assert_eq!(describe(&[1]), "singleton");
assert_eq!(describe(&[1, 2]), "pair");
assert_eq!(describe(&[1, 2, 3]), "many");
println!("Patterns work!");
}
// Approach 3: First, last, and middle
fn first_and_last(slice: &[i32]) -> Option<(i32, i32)> {
match slice {
[] => None,
[x] => Some((*x, *x)),
[first, .., last] => Some((*first, *last)),
}
}
fn middle(slice: &[i32]) -> &[i32] {
match slice {
[] | [_] => &[],
[_, middle @ .., _] => middle,
}
}
fn approach3() {
assert_eq!(first_and_last(&[]), None);
assert_eq!(first_and_last(&[1]), Some((1, 1)));
assert_eq!(first_and_last(&[1, 2, 3]), Some((1, 3)));
assert_eq!(middle(&[1, 2, 3, 4, 5]), &[2, 3, 4]);
println!("First/last and middle work!");
}
fn main() {
println!("=== Approach 1: Head/Tail ===");
approach1();
println!("\n=== Approach 2: Shape Matching ===");
approach2();
println!("\n=== Approach 3: First/Last/Middle ===");
approach3();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(sum(&[]), 0);
assert_eq!(sum(&[1, 2, 3]), 6);
}
#[test]
fn test_take() {
assert_eq!(take(0, &[1, 2, 3]), vec![]);
assert_eq!(take(2, &[1, 2, 3]), vec![1, 2]);
assert_eq!(take(5, &[1, 2]), vec![1, 2]);
}
#[test]
fn test_describe() {
assert_eq!(describe(&[]), "empty");
assert_eq!(describe(&[42]), "singleton");
}
#[test]
fn test_first_and_last() {
assert_eq!(first_and_last(&[10, 20, 30]), Some((10, 30)));
}
#[test]
fn test_middle() {
assert_eq!(middle(&[1, 2, 3, 4, 5]), &[2, 3, 4]);
assert_eq!(middle(&[1, 2]), &[]);
assert_eq!(middle(&[1]), &[]);
}
}
(* Example 114: Slice Patterns โ OCaml List Pattern Matching in Rust *)
(* Approach 1: Head/tail pattern matching *)
let rec sum = function
| [] -> 0
| x :: rest -> x + sum rest
let rec take n = function
| _ when n <= 0 -> []
| [] -> []
| x :: rest -> x :: take (n - 1) rest
let approach1 () =
assert (sum [1; 2; 3; 4; 5] = 15);
assert (take 3 [1; 2; 3; 4; 5] = [1; 2; 3]);
Printf.printf "Sum: %d, Take 3: %s\n" (sum [1;2;3;4;5])
(String.concat "," (List.map string_of_int (take 3 [1;2;3;4;5])))
(* Approach 2: Matching specific patterns *)
let describe = function
| [] -> "empty"
| [_] -> "singleton"
| [_; _] -> "pair"
| _ :: _ :: _ -> "many"
let approach2 () =
assert (describe [] = "empty");
assert (describe [1] = "singleton");
assert (describe [1; 2] = "pair");
assert (describe [1; 2; 3] = "many");
Printf.printf "Patterns work!\n"
(* Approach 3: First, last, and middle *)
let first_and_last lst =
match lst with
| [] -> None
| [x] -> Some (x, x)
| x :: rest ->
let last = List.nth rest (List.length rest - 1) in
Some (x, last)
let approach3 () =
assert (first_and_last [] = None);
assert (first_and_last [1] = Some (1, 1));
assert (first_and_last [1; 2; 3] = Some (1, 3));
Printf.printf "First and last work!\n"
let () =
approach1 ();
approach2 ();
approach3 ();
Printf.printf "โ All tests passed\n"
๐ Detailed Comparison
Comparison: Slice Patterns
Head/Tail Destructuring
OCaml:
๐ช Show OCaml equivalent
let rec sum = function
| [] -> 0
| x :: rest -> x + sum rest
Rust:
fn sum(s: &[i32]) -> i32 {
match s {
[] => 0,
[x, rest @ ..] => x + sum(rest),
}
}Shape Matching
OCaml:
๐ช Show OCaml equivalent
let describe = function
| [] -> "empty"
| [_] -> "singleton"
| [_; _] -> "pair"
| _ -> "many"
Rust:
fn describe(s: &[i32]) -> &str {
match s {
[] => "empty",
[_] => "singleton",
[_, _] => "pair",
_ => "many",
}
}First and Last (Rust advantage)
OCaml โ last is O(n):
๐ช Show OCaml equivalent
let first_and_last = function
| [] -> None
| [x] -> Some (x, x)
| x :: rest -> Some (x, List.nth rest (List.length rest - 1))
Rust โ last is O(1) with slice pattern:
fn first_and_last(s: &[i32]) -> Option<(i32, i32)> {
match s {
[] => None,
[x] => Some((*x, *x)),
[first, .., last] => Some((*first, *last)),
}
}