Daily Rust: Slice Patterns

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.

The code written in this article is available in the various playground links dotted throughout. Feel free to browse through and steal code or inspiration.

If you found this useful or spotted a bug in the article, let me know on the blog’s issue tracker!

Handling Plurality

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!");
}

(playground)

… will generate this output:

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

Matching the Start of a Slice

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(())
}

(playground)

Checking for Palindromes

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,
    }
}

(playground)

A Poor Man’s Argument Parser

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>,
}

(playground)

Irrefutable Pattern Matching

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];
}

(playground)

Conclusions

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++.