Rust 1.26 introduced a nifty little feature called Basic Slice Patterns which lets you pattern match on slices with a known length. Later on in Rust 1.42 , this was extended to allow using .. to match on “everything else”.

As features go this may seem like a small addition, but it gives developers an opportunity to write much more expressive code.

Contents 4-minute read
Published
Updated
Topics

Handling Plurality Link to heading

One of the simplest applications of slice patterns is to provide user-friendly messages by matching on fixed length slices.

Often it’s nice to be able to customise your wording depending on whether there were 0, 1, or many items. For example, this snippet…

fn print_words(sentence: &str) {
    let words: Vec<_> = sentence.split_whitespace().collect();

    match words.as_slice() {
        [] => println!("There were no words"),
        [word] => println!("Found 1 word: {}", word),
        _ => println!("Found {} words: {:?}", words.len(), words),
    }
}

fn main() {
    print_words("");
    print_words("Hello");
    print_words("Hello World!");
}

Run this example

… will generate this output:

There were no words
Found 1 word: Hello
Found 2 words: ["Hello", "World!"]

Matching the Start of a Slice Link to heading

The .. syntax is called a “rest” pattern and lets you match on (surprise, surprise) the rest of the slice.

According to the ELF Format , all ELF binaries must start with the sequence 0x7f ELF. We can use this fact and rest patterns to implement our own is_elf() check.

use std::error::Error;

fn is_elf(binary: &[u8]) -> bool {
    match binary {
        [0x7f, b'E', b'L', b'F', ..] => true,
        _ => false,
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    let current_exe = std::env::current_exe()?;
    let binary = std::fs::read(&current_exe)?;

    if is_elf(&binary) {
        print!("{} is an ELF binary", current_exe.display());
    } else {
        print!("{} is NOT an ELF binary", current_exe.display());
    }

    Ok(())
}

Run this example

Checking for Palindromes Link to heading

A very common introductory challenge in programming is to write a check for palindromes.

We can use the fact that the @ symbols binds a new variable to whatever it matches, and our ability to match on both the start and end of a slice to create a particularly elegant is_palindrome() function.

fn is_palindrome(items: &[char]) -> bool {
    match items {
        [first, middle @ .., last] => first == last && is_palindrome(middle),
        [] | [_] => true,
    }
}

Run this example

A Poor Man’s Argument Parser Link to heading

Another way you might want to use slice patterns is by “peeling off” desired prefixes or suffixes.

Although more sophisticated crates like clap and structopt exist, we can use this to implement our own basic argument parser.

fn parse_args(mut args: &[&str]) -> Args {
    let mut input = String::from("input.txt");
    let mut count = 0;

    loop {
        match args {
            ["-h" | "--help", ..] => {
                eprintln!("Usage: main [--input <filename>] [--count <count>] <args>...");
                std::process::exit(1);
            }
            ["-i" | "--input", filename, rest @ ..] => {
                input = filename.to_string();
                args = rest;
            }
            ["-c" | "--count", c, rest @ ..] => {
                count = c.parse().unwrap();
                args = rest;
            }
            [..] => break,
        }
    }

    let positional_args = args.iter().map(|s| s.to_string()).collect();

    Args {
        input,
        count,
        positional_args,
    }
}

struct Args {
    input: String,
    count: usize,
    positional_args: Vec<String>,
}

Run this example

Irrefutable Pattern Matching Link to heading

Although not technically part of the Slice Patterns feature, you can use pattern matching to destructure fixed arrays outside of a match or if let statement.

This can be useful in avoiding clunkier sequences based on indices which will never fail.

fn format_coordinates([x, y]: [f32; 2]) -> String {
    format!("{}|{}", x, y)
}

fn main() {
    let point = [3.14, -42.0];

    println!("{}", format_coordinates(point));

    let [x, y] = point;
    println!("x: {}, y: {}", x, y);
    // Much more ergonomic than writing this!
    // let x = point[0];
    // let y = point[1];
}

Run this example

Conclusions Link to heading

As far as features go in Rust slice patterns aren’t overly complex but when used appropriately, they can really improve the expressiveness of your code.

This was a lot shorter than my usual deep dives, but hopefully you learned something new. Going forward I’m hoping to create more of these Daily Rust posts, copying shamelessly from Jonathan Boccara’s Daily C++ .