๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Sequence pattern matching`[]` and `x :: rest` on lists`[]`, `[x]`, `[h, t @ ..]` on slices
Underlying data structureLinked list (cons cells)Contiguous memory (slice `&[T]`)
Rest binding`match lst with \x :: rest ->``[head, rest @ ..]`
Random access costO(n) for listsO(1) for slices
ExhaustivenessCompiler-checkedCompiler-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)),
 }
}