Slice patterns in Rust
Coming from the front-end world, where I use TypeScript on a daily basis, I find destructuring assignment, along with rest and spread syntax for unpacking and expanding arrays and objects, really helpful in writing concise and expressive code.
When I started to learn Rust, I immediately fell in love with its pattern matching feature, because it not only allows you to work with enums in ergonomic and safe manner, but also write really expressive and elegant code. Paired with destructuring and rest syntax, it becomes extremely powerful.
Note: these patterns work only with slices or arrays, so you can’t, for instance, apply them to plain vectors, since their size is not known at compile-time.
Now, let’s see some examples.
Taking heads & tails off!
head
Simplest function of the kin for returning the first element of a list slice. Since I wanted it to be generic (to some extent), its signature looks a bit more involved than it could be… Anyway, that’s not gonna change the fact, that we destructure the slice to take the head
and return a copy of it, while ignoring the rest ..
of the slice.
Playground with tests
In TypeScript, we can implement this in a similar, but much less safer fashion, or make use of libraries like purify-ts. It provides us with some handy algebraic data structures, e.g. Maybe<T>
, which in this case is identical to the Rust’s Option<T>
.
tail
What about tails? Pretty much the same story, but with a binding (note that @
sigil).
Bindings allow us to, well, bind whatever matches the pattern to a variable, and then use it in a guard predicate, and in the body of the matching branch. In our function we simply bind the rest of the slice to the tail
variable and then return it, converting to a Vec<T>
.
Playground with tests
The same in TypeScript land with purify-ts:
Rusty tenet
Checking whether a string is palindrome or not, is a very common challenge, so why not solve it?
Again, we make use of binding, and match on both the start and end of a slice to create a really elegant solution with recursion.
Playground with tests
Looks neat, if you ask me!
In TypeScript, unfortunately, it gets way more imperative, because TypeScript doesn’t allow us to destructure an array as flexible, as Rust does. I admit, this is not the best solution in terms of performance, but at least it roughly maps to what we have above in Rust.
A little trick
There’s another handy trick we can use within slice patterns. We can not only destructure slices or arrays and bind matches to variables, but also match different variants while doing destructuring.
Let’s say, we have some fictional binary format. It has two versions, and each matches to its own sequence of bytes, though they are quite similar. V1 can’t be processed, and V2 can.
- If a sequence starts with
0x88
followed byA
orB
, then it’s V1. - If a sequence starts with
0x88
followed byX
, then it’s V2.
Playground with tests
In the first branch we first check if slice starts with 0x88
, and then check for two alternatives: b'A'
and b'B'
.
In TypeScript something similar would look less terse and pretty.
Nothing special here, except using enums, which most TypeScript devs don’t recommend to use (and for a good reason, especially before 5.0), but they’ve got an overhaul in 5.0 and finally are typesafe.
Conclusion
As you can see, slice patterns aren’t that complex compared to other features in Rust, and they can greatly improve the expressiveness of your code. I really like how fine-grain you can be when defining patterns, and it’s one of the functional features — among others, like iterators and immutability by default — that probably outmatches (pun intended) the very same feature in Haskell.