In Rust, there is this feature known as “pattern matching”, whereby you can take apart a piece of structured data by writing out patterns which bind to parts of it. This process is known as “destructuring”, because you’re breaking a structure into parts.
But there’s this old feature which I’ve unilaterally decided to call “restructuring patterns”:
let x = Some(10);
match x {
Some(ref inner) => {
// Look at the type!
let _: &i32 = inner;
},
None => unreachable!(),
}
See that ref
keyword? It adds structure to the value inner
when taking it out of x
. Instead of moving an i32
out of x
, we’re taking a reference to inside it!
Two keywords are valid in that position: ref
and mut
, and they do different things. mut
marks a binding as mutable, and can be used in combination with ref
, as ref mut
, to introduce a &mut
to the structure of the binding they’re next to instead of a &
.
This is distinct from using &
and &mut
themselves in a pattern, as these do the normal work of destructuring, working as dereference operators in that position:
let x = Some(&10);
match x {
Some(&inner) => {
// Look at the type!
let _: i32 = inner;
},
None => unreachable!(),
}
Basically, using a type in a pattern lets you rip apart that type, like &x
, &mut x
, Json(x)
, etc., and the super special ref
keyword does the opposite.
Now, that mental model is good enough that you can pretty much run with it and it won’t steer you wrong. But it’s not the whole story.
The modern approach to matching on things with references involved is covered well by the Match Ergonomics RFC. It defines a notion of “binding mode”, which is the actual thing we’re controlling when we use the ref
keyword, and that fact in turn is why we can’t use ref ref inner
as a pattern. (Not that I’d ever thought to write that particular pattern before working on this post.)
So, when you’re match
ing on something, Rust examines the type of the value being matched on, and decides on a default binding mode from one of these:
move
, which takes ownership of the value being pattern matchedref
, which takes a shared borrow from the value being matchedref mut
, which takes a unique borrow from the value being matchedThis is chosen based on whether the type of the value being matched has an outer layer of &mut
, which pushes toward a default binding mode of ref mut
, or a &
, which forces the default binding mode to be ref
. If neither reference type is present, the default binding mode will be move
.
This default binding mode is used if, and only if, you don’t write a reference as the outer layer of your pattern. This used to be a compiler error, but now it’s what we basically always do!
With that in mind, the ref
keyword is really a tool to change the binding mode from a default of move
to use ref
/ ref mut
instead. In this model, it really doesn’t make sense to nest it in the fashion of ref ref inner
, since it’s a switch with only two or three states where it’s used.
With all that said, wouldn’t it be neat if “restructuring patterns” were actually a first-class feature? I don’t have an idea off the top of my head what that’d be good for, but the lang-dev in me says we should investigate. If not for Rust, maybe some hypothetical other language?
I was motivated to write this post by a writing group we formed in the RPLCS Discord. Other posts by our group are listed here: https://www.catmonad.xyz/writing_group/.