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