Today's match ergonomics have a series of edge cases that make it hard to predict and understand. In this RFC I propose a reframe of match ergonomics that (I hypothesize) is a better match for intuition. This is a breaking change that requires an edition.
This proposal, as well as today's stable behavior, RFC3627, and several variants, can be experimented with in a web tool I wrote, inspired by (and checked against) TC's match-ergonomics-formality
tool. The tool is an interactive web app that allows setting various options, comparing concrete cases, and showing rulesets in the style of this proposal.
I'd like to thank everyone who participated in the various meetings about match ergonomics, and particularly TC and Waffle, for taking the time to understand this complex domain, putting forward proposals, having brilliant insights, and working to make rust better. This proposal could not have come to exist without you.
Today's match ergonomics operate based on the "in-memory type" i.e. the type of the place that a subpattern matches on. This type is different than the "user-visible type" i.e. the type that would be obtained if the current pattern was replaced with a binding.
When thought of in terms of the "user-visible type", match ergonomics exhibit surprising edge cases:
mut
sometimes dereferences the value;ref
/ref mut
is sometimes a no-op;x: &T
, writing &x
inside the pattern has context-dependent effects: it can be a type error, work as expected, or dereference the value twice.RFC 3627 recognizes these issues and seeks to address them, but doesn't solve them all:
mut x
is disallowed inconsistently:The starting point of this RFC is the claim that these behaviors do not match intuition, and the hypothesis that this comes from the fact that RFCs 2005 and 3627 operate on the in-memory type, with extra hidden state called the "binding mode".
This RFC proposes to typecheck patterns based on the user-visible type instead, which mostly requires no such hidden state and solves all these surprises.
The first idea of pattern-matching is that each type can be matched/deconstructed using the same notation used to construct it:
Option<T>
can be matched with patterns Some(p)
and None
;Struct { field1: p1, field2: p2, .. }
;&T
can be matched with pattern &p
;&mut T
can be matched with pattern &mut p
;Match ergonomics extend this behavior: a non-reference constructor is allowed to match through arbitrary layers of references. The references "slip into" the fields of the constructors, and nested references merge into a single one. See "reference-level explanation" for a precise formulation of this idea.
For example:
Compared to stable rust:
mut
simply makes the binding mutable;ref
/ref mut
always borrows (but we may forbid the case where this creates an invisible temporary, see next section);x: &T
, writing &x
inside the pattern always dereferences the value to x: T
;&p
is allowed on &mut T
.In this section, we describe the typechecking of patterns using typing rules written in the typical notation of type theory papers: each rule is to be read bottom to top, as "for <bottom>
to hold, <top>
must (recursively) hold". The top is a comma-separated list of typing predicates. An empty top means that the rule applies unconditionally. If no rule applies to a given case (e.g. &mut x: &T
), this means that the case counts as a type error.
In these rules, C
denotes a constructor for the type CT
with fields of types CT0
, CT1
, etc; p
/p0
/p1
denote arbitrary patterns, T
denotes an arbitrary type; x
denotes an identifier (for a binding).
Without match ergonomics, the typechecking of patterns can be described by the following set of rules:
Match ergonomics (as proposed in this RFC) adds the following extra rules:
Note that today's (RFC2005) match ergonomics cannot be directly described in this way, as there is extra information (the binding mode) to track. Neither can RFC3627. The rules formalism can be extended to track this extra information (as is done in the tool), but isn't as natural a fit for these rulesets.
Note also that the following is allowed:
which requires x
to borrow a temporary, in a way equivalent to:
As this may be surprising (especially for ref mut
), we propose to add a warn-by-default lint that suggests removing the ref
/ref mut
, or forbid this altogether.
Note finally that the following is allowed:
which cannot be desugared into a form without match ergonomics, as this would require something like mut ref x
. The current proposal purposefully steps away from the "desugaring" framing as it is no longer necessary to explain the behavior of patterns, so this isn't seen as a problem.
This proposal changes the meaning of many patterns; as such, this is a breaking change.
These rules would only take effect on the next edition. The migration lint will modify all patterns to lie in the subset of rules that are common to all editions. At worst this involves fully desugaring match ergonomics, much like the proposed RFC3627 migrations.
Previous editions only get the backwards-compatible changes. This means allowing &p
patterns on &mut T
types, and allowing &p
/&mut p
patterns to consume the binding mode as if it was a reference in places where there would otherwise be a type error. In the language of RFC3627, that's rules 4 and 5 (not rule 3 since it is not part of this proposal). In terms of tool options (described below), this is allow_ref_pat_on_ref_mut + EatBoth + fallback_to_outer + eat_inherited_ref_alone
.
This is a counter-proposal to RFC3627. Both proposals aim to improve match ergonomics and require breaking changes over an edition. They mostly agree on what's confusing in today's match ergonomics.
The proposals differ in execution. In this section, we will compare in more detail this proposal against RFC3627 and its variants.
Note that contrary to RFC3627, the current proposal doesn't really have degrees of freedom; the simplicity of the formalism forces a single option. The degrees of freedom are the extra hard errors we could add as well as RFC3627's rule 3, which we discuss below. These are listed in the "unresolved questions" section.
To go more into detail, apart from RFC3627's rule 3 which has its own section below, the differences between the various proposals all lie in two areas:
mut
, ref
and ref mut
) in the presence of inherited references (aka non-move
binding mode);move
binding mode).The fact that these are the only variables at play was discovered by formalizing all proposals in the tool. Each of these alternatives corresponds to an option that can be set in the tool.
The real crux of the difference between the proposals is what "type" they care about. RFC3627 inspects the "in-memory type" i.e. the type of the place that is being matched on. This proposal inspects the "user-visible type" i.e. the type that would be obtained if the current pattern was a binding.
&p
on &mut T
All the proposals agree on this: it is desireable for &p
to match on &mut T
as if it was &T
. This is only mentioned for completeness and future reference.
The tool calls this option allow_ref_pat_on_ref_mut
.
mut
binding modifierTake the case of let [mut x]: &[T] = ...;
. Without the mut
, all proposals agree that x: &T
. With the mut
, proposals differ in how to treat the inherited reference aka non-move
binding mode. There are only three options:
x: T
;x: &T
binding mutable.Option 3 seems unanimously desireable. Its drawback is that it prevents the desugaring of such patterns into patterns that don't use match ergonomics, because that would require a new binding modifier (that we could call mut ref
).
This is why RFC3627 chose option 2. Both 2 and 3 are deemed acceptable in the frame of the current proposal; 1 isn't.
These three alternatives can be set with set mut_binding_on_inherited
in the tool.
ref
/ref mut
binding modifiersTake the case of let [ref x]: &[T] = ...;
. Without the ref
, all proposals agree that x: &T
. With the ref
, proposals differ in how to treat the inherited reference aka non-move
binding mode. There are only three options:
x: &T
;&T
value. This means x: &&T
and requires creating a temporary to store the &T
value.Stable rust and RFC3627 (including all its variants) agree on option 1. The typing-rules framework disagrees as option 1 requires tracking the binding mode.
Option 3 is probably what most rust users would expect at first. The drawback of option 3 is that it requires an invisible temporary. Both 2 and 3 are deemed acceptable in the frame of the current proposal.
These three alternatives can be set with set ref_binding_on_inherited
in the tool.
This is the most subtle part. The setup is as follows: a reference pattern &{mut}p
is matched against a doubly-referenced type &{mut}&{mut}T
where the outer reference is inherited (i.e. came from an application of one of the ConstructorRef
rules). In the framework of RFC3627, this is the same as a &{mut}p
pattern matched against a reference type &{mut}T
with a ref
or ref mut
binding mode.
There are two levels of reference to consider: the inherited (aka outer aka binding mode) one, and the "real" (aka inner) one. Proposals differ on which should be considered first. The tool calls this option inherited_ref_on_ref
. The possible values are:
EatOuter
: match the pattern against the outer reference only;EatInner
: match the pattern against the inner reference first; if they match then remove the inner one and keep the outer one;EatBoth
: match the pattern against the inner reference; if they match then remove both.For example:
Separately, in case of a mutability mismatch, some proposals propose to try again in EatOuter
mode. The tool represents this with a boolean option fallback_to_outer
. This is not relevant if the base option is already EatOuter
of course.
For example:
EatBoth + !fallback_to_outer
;EatOuter
, as the other options require tracking the binding mode;EatInner + fallback_to_outer
EatInner + !fallback_to_outer
;EatBoth + fallback_to_outer
.In principle EatInner + fallback_to_outer
should allow more cases than EatOuter
alone, but combined with allow_ref_pat_on_ref_mut
this is not always the case, and either way this leads to surprising behaviors such as the ones noted in the introduction:
mut x
is disallowed inconsistently:RFC 3627's rule 3 says (in our frame): "If we've previously matched against a shared reference in the scrutinee, downgrade any inherited &mut
to &
". In the tool, this option is named downgrade_mut_inside_shared
.
This ensures that a &mut
pattern is only allowed if mutable access to the value is indeed possible (at least as far as match ergonomics are concerned). E.g.:
This rule is somewhat orthogonal to the current proposal. It is not included because it requires hidden state; it is however compatible and could be added without making the mental model much more complicated.
This option is not backwards-compatible either way: &[&mut x]: &&mut [T]
is allowed without but becomes an error with it, and conversely the example above is allowed with it but is an error without.
Some(mut x): &Option<T>
be banned because it cannot be desugared?Some(ref x): &Option<T>
be banned because of the invisible temporary?&mut
to &
"?