Goal: Be able to match through a Deref
or DerefMut
smart pointer ergonomically.
Prior discussions include:
From these and my own considerations, I have come up with two related proposals.
We have a working implementation (without exhaustiveness checking) of one of the proposals available under the deref_patterns
experimental feature gate.
&T
and a smart pointer should be made as small as possible;
Rc
to Arc
should require minimal code changes;Q: should &
vs &mut
indicate the mutability of the type, or of the borrow?
A: today is mostly type, ergo2024 makes it sometimes borrow. E.g. let &mut (ref x) = &mut place;
shares place
.
The two proposals differ in syntax but share how they work: a &<pat>
/*<pat>
/deref <pat>
/etc pattern would be allowed for stdlib smart pointers like Box
or Rc
, where <pat>
would match on the pointed-to value.
Everything else about patterns works the same: we can nest them, we can get immutable or mutable access, they are subject to exhaustiveness checking and the dead arm lint.
Before we discuss the two syntactic options, let's start with some common design details.
The feature would be enabled for the following std types: Box
, Rc
, Arc
, Vec
, String
, Cow
, Pin
, ManuallyDrop
, Ref
, RefMut
, LazyCell
, LazyLock
. This is sound because all those impls are sufficiently idempotent.
Extending the feature to user-defined Deref
impls is outside the scope of this proposal.
Patterns are treated exhaustively as expected:
This is sound because we only enable the feature for a trusted set of types whose Deref
impls behave well enough.
Some Deref
std types like Cow
can be matched normally. For simplicity we forbid mixing deref and normal patterns for now.
The precedent with match ergonomics and the general way rust tends to work suggests that implicit deref patterns (if we want them) should desugar into an explicit form.
Moreover, we need explicit syntax to disambiguate cases like:
As such, both proposals focus on the explicit syntax. Implicit patterns are an optional extension.
We follow how the rest of rust works for matches and Deref
: we work on places.
This means that &<pat>
/deref <pat>
operates on a place x: P
where P: Deref<Target=T>
, and matches <pat>
against the place *x
of type T
.
It does not operate on a place of type &P
/&mut P
(except insofar as match ergonomics make it seem like it does). It also does not return a place of type &T
/&mut T
(again modulo match ergonomics). As we will see in Unresolved Questions, this poses an issue for string literals. Yet this is the consistent choice wrt the rest of rust.
&<pat>
Because of how &
works today, we don't really have much choice:
As you can see, this proposal isn't very convenient without implicit deref patterns.
As could be expected, &<pat>
calls deref
and &mut <pat>
calls deref_mut
.
Interestingly, because &mut T: Deref
, this allows matching on &mut T
with a &<pat>
pattern:
DerefMove
Because we distinguish &
and &mut
the way we do, to move out we'd probably want some other syntax. Maybe move <pat>
, maybe *<pat>
, idk.
The match ergonomics 2024 proposal includes some possible changes to &<pat>
patterns. Depending on the exact choices made, this could conflict with deref patterns.
For example:
x: u32
.&<pat>
everywhere" only, x: Box<u32>
(which gives a move error).x: u32
.x: Box<u32>
.Hence combining the features could accidentally create a breaking change.
The safest choice is to disallow &<pat>
deref patterns in the presence of match-ergonomics-inherited references, and to disallow "&<pat>
everywhere" on Deref
types (should be fine because Deref
types are rarely Copy
). That gives us freedom to land either feature in any order. We can relax restrictions once both are stable.
For custom Deref
, this may mean that implementing Deref
on a Copy
type can break downstream crates:
deref <pat>
, *<pat>
, or <Pointer>(<pat>)
For the sake of presentation, I'll use deref
. Any syntax different from &
can work the same.
This does not require implicit deref patterns to be practical.
Mutability is inferred from the kinds of bindings we do inside the pattern.
DerefMove
Compatible
box_patterns
feature entirely.Here are the proposals I've seen:
deref <pat>
;*<pat>
;box <pat>
(already reserved keyword);Box(<pat>)
/Rc(<pat>)
/String(<pat>)
โฆ type-specific pattern.Note that deref <pat>
would require reserving a keyword, since deref(x, y)
could be a tuple struct pattern and deref ::Type
could be a path.
Note that Pointer(<pat>)
would require some rule to not conflict with tuple struct syntax. Maybe we disallow it on tuple structs, maybe some visibility-based hack. Also it doesn't work in generic contexts, unless we allow T(<pat>)
for generic T
.
Option 1: &<pat>
;
DerefMove
;&
pattern on something that isn't a reference could feel weird (e.g. matches!(Rc::new(true), &true)
).Option 2: deref <pat>
, *<pat>
or Pointer(<pat>)
.
deref
would require a keyword i.e. new edition;*<pat>
goes the wrong way around, e.g. &*<pat>
looks like a reborrow but is actually two dereferences;Pointer(<pat>)
doesn't fit well with the implicit syntax;Pointer(<pat>)
conflicts with tuple structs.Compatibility matrix:
Option | +implicit | only explicit | +move | +custom Deref |
works in all editions | consistent with today's rust |
---|---|---|---|---|---|---|
&<pat> |
Image Not Showing
Possible Reasons
|
noisy |
Image Not Showing
Possible Reasons
|
iffy with ergonomics 2024 |
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
deref <pat> |
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
sort of |
Pointer(<pat>) |
weird |
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
*<pat> |
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
Image Not Showing
Possible Reasons
|
A fun consequence of how we deal with places.
A similar issue exists today when matching with constants of type &T
(playground):
According to @compiler-errors
this should be easy to solve for the specific case of string literals so let's ignore for now.
As discussed above, we could extend match ergonomics to add implicit deref patterns as needed.
Today, when a concrete pattern p
which isn't of the form &<pat>
is used to match on a place x: &T
, we adjust the binding mode and keep matching p
on the place *x
.
We would extend this behavior to places x
of type P
where P: Deref
is one of the supported std types. This would insert implicit deref patterns. E.g.
Deref
I (Nadri) am reasonably confident that we can make this sound (cannot cause UB) for arbitrary user-defined Deref
s as long as we disable exhaustiveness (i.e. require a _
arm).
That said, there remain many design questions:
For that reason they're not included in the current proposal. That said, I personally think they're too cool to forbid, even if they stay perma-unstable or behind an opt-in unsafe trait.
Note that this is in tension with implicit deref patterns, as this means patterns could implicitly run arbitrary user code. We could e.g. force explicit patterns when the Deref
impl is untrusted.
We could implement this today for Box
, to match the existing deref magic (and replace box patterns). For other types, this would require a solution to the tricky DerefMove
design issue.
In either case, this requires a compatible syntax for the explicit form as discussed above.
We could allow mixing normal and deref patterns:
(note: the hypothetical impl Cow: DerefMut
that clones implicitly would stop Cow
from being eligible for exhaustiveness)
The two types of patterns are treated independently. In particular, exhaustiveness won't try to figure out that this is exhaustive: