About a week ago, there was an item in This Week in Rust (issue 320) that caught my eye under the Tracking Issues & PRs heading:
[disposition: merge] Stabilize ~#![feature(slice_patterns)]~ in 1.42.0.
Aww, yeah! I've been waiting for this for literal years, so you better believe that was a good day! I mentioned this in my Rust 2020 post as one of the things I'm the most excited to see this year, so now that it's getting stabilized soon (2020-03-12), let's make sure we're prepared!
About slice patterns
Most of the information contained in this section can also be found in the RFC on GitHub. The RFC is very thorough and gives lots of examples, so go give it a read if you want to know more about the feature!
We've had some form of slice matching on stable Rust for a while now, but without this feature, the form of matching you can do is rather limited. With an array of a known length, you can destructure and match as you please, but for slices of an unknown length, you must provide a fallback because there is no way to cover all the potential cases in a match
expression. Also, quite importantly: there is no way to bind variables to subslices. This feature finally opens the gates to subslice and subarray matching, mitigating both the above issues, and making slice patterns immensely more powerful.
Two flavors
There are two syntactic flavors for the new subslice patterns: one for when you want to bind a subslice to a variable, and one for when you just want to denote that there are elided elements. Both flavors use the ..
pattern (referred to as a rest pattern) to match a variable number of elements. The number of elements matched depend on the length of the array or slice and the number of matching elements before and after in the match.
Matching without binding
Looking at the first flavor, matching without binding, we get introduced to the ..
pattern straight away:
fn f<T>(xs: &[T])
where
T: std::fmt::Debug,
{
match xs {
// the slice has at least two variables.
// we bind the first and last items of
// the slice to `x` and `y`, respectively
[x, .., y] => {
println!("First and last: {:?} and {:?}.", x, y)
}
// the slice has a single item: `x`
[x] => {
println!("the slice has a single item: {:?}.", x)
}
// the slice is empty
[] => {
println!("Got an empty slice.")
}
}
}
Remember that ..
can match any number of elements, including 0. This means that the first pattern matches anything that has at least two items. You can also use the pattern without 'delimiting' it on both ends, such as if you wanted to implement these two functions for getting the first and last element of a slice:
fn first<T>(xs: &[T]) -> Option<&T> {
match xs {
[x, ..] => Some(x),
[] => None,
}
}
fn last<T>(xs: &[T]) -> Option<&T> {
match xs {
[.., x] => Some(x),
[] => None,
}
}
Notice how both of these functions pick out a single element of the slice (first and last respectively) and ignore the rest. Because ..
matches 0 or more elements, the first pattern in both functions matches slices with one or more elements.
Matching and binding a subslice
The other flavor lets you bind a subslice to a value, which takes slice patterns and cranks the power level up another notch or two. The binding is done using the @
operator.
Imagine for instance that we want to write a sum
function. That could be done as such:
fn sum(xs: &[i32]) -> i32 {
match xs {
[] => 0,
[x, xs @ ..] => x + sum(xs),
}
}
In the example above, if the slice is not empty, we take the first element, x
, and add it to the result of summing the rest of the list, xs
. With Rust already having a sum
method on iterators, this function is pretty redundant, but it makes a good example of how to bind and use subslices.
Another example would be to get the middle element of a slice if the slice has an odd number of elements. If the slice is empty or has an even number of elements, return None
:
fn middle<T>(xs: &[T]) -> Option<&T> {
match xs {
// ignore the first and last element.
// recurse with what's in between.
[_, inner @ .., _] => middle(inner),
// one element! got it!
[x] => Some(x),
// oops! there's nothing here.
[] => None,
}
}
Here we iterate through the slice from both sides, continuously picking off one element at the start and one element at the end. Whatever is left in the middle (if there are at least two elements) gets assigned to xs
and used as input to another step through the function. Once we have either one or zero elements left, we have our answer.
Why this is a big deal
It might come across as somewhat strange that I'm so enthused about a feature that may seem rather small, but it's one of those quality of life things that I find myself running into all the time. Being used to Haskell and their pattern matching behavior, I always forget how cumbersome it is to match on an arbitrary slice in Rust. Up until now, we've had the ~split_first~ method (and split_at
) on slices, which I can never remember the name of, which returns an Option
, and which doesn't let you do arbitrary match-stuff (such as using match guards, for instance). The new slice_patterns
feature is a major step up in that regard.
The other thing that I'm super jazzed about? Being able to match on the end of a slice. Not only can you pick off items from either end of the slice, but you can also make sure that the slice ends in a certain value or series of values.
In short, I think this is an amazing addition to stable Rust. Hats off to all the people that have made it possible. Now go read the RFC and look out for all the other cool stuff they're talking about (arbitrarily nested OR patterns? Oh, my!).