Skip to content

Commit 7b7e8b9

Browse files
committed
Merge RFC 3637: Guard patterns
The FCP for RFC 3637 completed on 2024-08-31 with a disposition to merge. Let's merge it.
2 parents 86450a4 + ab79e9e commit 7b7e8b9

File tree

1 file changed

+346
-0
lines changed

1 file changed

+346
-0
lines changed

text/3637-guard-patterns.md

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
- Feature Name: `guard_patterns`
2+
- Start Date: 2024-05-13
3+
- RFC PR: [rust-lang/rfcs#3637](https://github.com/rust-lang/rfcs/pull/3637)
4+
- Tracking Issue: [rust-lang/rust#129967](https://github.com/rust-lang/rust/issues/129967)
5+
6+
# Summary
7+
8+
[summary]: #summary
9+
10+
This RFC proposes to add a new kind of pattern, the **guard pattern.** Like match arm guards, guard patterns restrict another pattern to match only if an expression evaluates to `true`. The syntax for guard patterns, `pat if condition`, is compatible with match arm guard syntax, so existing guards can be superceded by guard patterns without breakage.
11+
12+
# Motivation
13+
14+
[motivation]: #motivation
15+
16+
Guard patterns, unlike match arm guards, can be nested within other patterns. In particular, guard patterns nested within or-patterns can depend on the branch of the or-pattern being matched. This has the potential to simplify certain match expressions, and also enables the use of guards in other places where refutable patterns are acceptable. Furthermore, by moving the guard condition closer to the bindings upon which it depends, pattern behavior can be made more local.
17+
18+
# Guide-level explanation
19+
20+
[guide-level-explanation]: #guide-level-explanation
21+
22+
Guard patterns allow you to write guard expressions to decide whether or not something should match anywhere you can use a pattern, not just at the top level of `match` arms.
23+
24+
For example, imagine that you're writing a function that decides whether a user has enough credit to buy an item. Regular users have to pay 100 credits, but premium subscribers get a 20% discount. You could implement this with a match expression as follows:
25+
26+
```rust
27+
match user.subscription_plan() {
28+
Plan::Regular if user.credit() >= 100 => {
29+
// Complete the transaction.
30+
}
31+
Plan::Premium if user.credit() >= 80 => {
32+
// Complete the transaction.
33+
}
34+
_ => {
35+
// The user doesn't have enough credit, return an error message.
36+
}
37+
}
38+
```
39+
40+
But this isn't great, because two of the match arms have exactly the same body. Instead, we can write
41+
42+
```rust
43+
match user.subscription_plan() {
44+
(Plan::Regular if user.credit() >= 100) | (Plan::Premium if user.credit() >= 80) => {
45+
// Complete the transaction.
46+
}
47+
_ => {
48+
// The user doesn't have enough credit, return an error message.
49+
}
50+
}
51+
```
52+
53+
Now we have just one arm for a successful transaction, with an or-pattern combining the two arms we used to have. The two nested patterns are of the form
54+
55+
```rust
56+
pattern if expr
57+
```
58+
59+
This is a **guard pattern**. It matches a value if `pattern` (the pattern it wraps) matches that value, _and_ `expr` evaluates to `true`. Like in match arm guards, `expr` can use values bound in `pattern`.
60+
61+
## For New Users
62+
63+
For new users, guard patterns are better explained without reference to match arm guards. Instead, they can be explained by similar examples to the ones currently used for match arm guards, followed by an example showing that they can be nested within other patterns and used outside of match arms.
64+
65+
# Reference-level explanation
66+
67+
[reference-level-explanation]: #reference-level-explanation
68+
69+
## Supersession of Match Arm Guards
70+
71+
Rather than being parsed as part of the match expression, guards in match arms will instead be parsed as a guard pattern. For this reason, the `if` pattern operator must have lower precedence than all other pattern operators.
72+
73+
That is,
74+
75+
```rs
76+
// Let <=> denote equivalence of patterns.
77+
78+
x @ A(..) if pred <=> (x @ A(..)) if pred
79+
&A(..) if pred <=> (&A(..)) if pred
80+
A(..) | B(..) if pred <=> (A(..) | B(..)) if pred
81+
```
82+
83+
## Precedence Relative to `|`
84+
85+
Consider the following match expression:
86+
87+
```rust
88+
match foo {
89+
A | B if c | d => {},
90+
}
91+
```
92+
93+
This match arm is currently parsed as `(A | B) if (c | d)`, with the first `|` being the or-operator on patterns and the second being the bitwise OR operator on expressions. Therefore, to maintain backwards compatability, `if` must have lower precedence than `|` on both sides (or equivalently, for both meanings of `|`). For that reason, guard patterns nested within or-patterns must be explicitly parenthesized:
94+
95+
```rust
96+
// This is not an or-pattern of guards:
97+
a if b | c if d
98+
<=> (a if (b | c)) if d
99+
100+
// Instead, write
101+
(a if b) | (c if d)
102+
```
103+
104+
## In Assignment-Like Contexts
105+
106+
There's an ambiguity between `=` used as the assignment operator within the guard
107+
and used outside to indicate assignment to the pattern (e.g. in `if let`)
108+
Therefore guard patterns appearing at the top level in those places must also be parenthesized:
109+
110+
```rust
111+
// Not allowed:
112+
let x if guard(x) = foo() {} else { loop {} }
113+
if let x if guard(x) = foo() {}
114+
while let x if guard(x) = foo() {}
115+
116+
// Allowed:
117+
let (x if guard(x)) = foo() {} else { loop {} }
118+
if let (x if guard(x)) = foo() {}
119+
while let (x if guard(x)) = foo() {}
120+
```
121+
122+
Therefore the syntax for patterns becomes
123+
124+
> **<sup>Syntax</sup>**\
125+
> _Pattern_ :\
126+
> &nbsp;&nbsp; &nbsp;&nbsp; _PatternNoTopGuard_\
127+
> &nbsp;&nbsp; | _GuardPattern_
128+
>
129+
> _PatternNoTopGuard_ :\
130+
> &nbsp;&nbsp; &nbsp;&nbsp; `|`<sup>?</sup> _PatternNoTopAlt_ ( `|` _PatternNoTopAlt_ )<sup>\*</sup>
131+
132+
With `if let` and `while let` expressions now using `PatternNoTopGuard`. `let` statements and function parameters can continue to use `PatternNoTopAlt`.
133+
134+
## Bindings Available to Guards
135+
136+
The only bindings available to guard conditions are
137+
138+
- bindings from the scope containing the pattern match, if any; and
139+
- bindings introduced by identifier patterns _within_ the guard pattern.
140+
141+
This disallows, for example, the following uses:
142+
143+
```rust
144+
// ERROR: `x` bound outside the guard pattern
145+
let (x, y if x == y) = (0, 0) else { /* ... */ }
146+
let [x, y if x == y] = [0, 0] else { /* ... */ }
147+
let TupleStruct(x, y if x == y) = TupleStruct(0, 0) else { /* ... */ }
148+
let Struct { x, y: y if x == y } = Struct { x: 0, y: 0 } else { /* ... */ }
149+
150+
// ERROR: `x` cannot be used by other parameters' patterns
151+
fn function(x: usize, ((y if x == y, _) | (_, y)): (usize, usize)) { /* ... */ }
152+
```
153+
154+
Note that in each of these cases besides the function, the condition is still possible by moving the condition outside of the destructuring pattern:
155+
156+
```rust
157+
let ((x, y) if x == y) = (0, 0) else { /* ... */ }
158+
let ([x, y] if x == y) = [0, 0] else { /* ... */ }
159+
let (TupleStruct(x, y) if x == y) = TupleStruct(0, 0) else { /* ... */ }
160+
let (Struct { x, y } if x == y) = Struct { x: 0, y: 0 } else { /* ... */ }
161+
```
162+
163+
In general, guards can, without changing meaning, "move outwards" until they reach an or-pattern where the condition can be different in other branches, and "move inwards" until they reach a level where the identifiers they reference are not bound.
164+
165+
## As Macro Arguments
166+
167+
Currently, `if` is in the follow set of `pat` and `pat_param` fragments, so top-level guards cannot be used as arguments for the current edition. This is identical to the situation with top-level or-patterns as macro arguments, and guard patterns will take the same approach:
168+
169+
1. Update `pat` fragments to accept `PatternNoTopGuard` rather than `Pattern`.
170+
2. Introduce a new fragment specifier, `pat_no_top_guard`, which works in all editions and accepts `PatternNoTopGuard`.
171+
3. In the next edition, update `pat` fragments to accept `Pattern` once again.
172+
173+
# Drawbacks
174+
175+
[drawbacks]: #drawbacks
176+
177+
Rather than matching only by structural properties of ADTs, equality, and ranges of certain primitives, guards give patterns the power to express arbitrary restrictions on types. This necessarily makes patterns more complex both in implementation and in concept.
178+
179+
# Rationale and alternatives
180+
181+
[rationale-and-alternatives]: #rationale-and-alternatives
182+
183+
## "Or-of-guards" Patterns
184+
185+
Earlier it was mentioned that guards can "move outwards" up to an or-pattern without changing meaning:
186+
187+
```rust
188+
(Ok(Ok(x if x > 0))) | (Err(Err(x if x < 0)))
189+
<=> (Ok(Ok(x) if x > 0)) | (Err(Err(x) if x < 0))
190+
<=> (Ok(Ok(x)) if x > 0) | (Err(Err(x)) if x < 0)
191+
// Cannot move outwards any further, because the conditions are different.
192+
```
193+
194+
In most situations, it is preferable to have the guard as far outwards as possible; that is, at the top-level of the whole pattern or immediately within one alternative of an or-pattern.
195+
Therefore, we could choose to restrict guard patterns so that they appear only in these places.
196+
This RFC refers to this as "or-of-guards" patterns, because it changes or-patterns from or-ing together a list of patterns to or-ing together a list of optionally guarded patterns.
197+
198+
Note that, currently, most patterns are actually parsed as an or-pattern with only one choice.
199+
Therefore, to achieve the effect of forcing patterns as far out as possible guards would only be allowed in or-patterns with more than one choice.
200+
201+
There are, however, a couple reasons where it could be desirable to allow guards further inwards than strictly necessary.
202+
203+
### Localization of Behavior
204+
205+
Sometimes guards are only related to information from a small part of a large structure being matched.
206+
207+
For example, consider a function that iterates over a list of customer orders and performs different actions depending on the customer's subscription plan, the item type, the payment info, and various other factors:
208+
209+
```rust
210+
match order {
211+
Order {
212+
// These patterns match based on method calls, necessitating the use of a guard pattern:
213+
customer: customer if customer.subscription_plan() == Plan::Premium,
214+
payment: Payment::Cash(amount) if amount.in_usd() > 100,
215+
216+
item_type: ItemType::A,
217+
// A bunch of other conditions...
218+
} => { /* ... */ }
219+
// Other similar branches...
220+
}
221+
```
222+
223+
Here, the pattern `customer if customer.subscription_plan() == Plan::Premium` has a clear meaning: it matches customers with premium subscriptions. Similarly, `Payment::Cash(amount) if amount.in_usd() > 100` matches cash payments of amounts greater than 100USD. All of the behavior of the pattern pertaining to the customer is in one place, and all behavior pertaining to the payment is in another. However, if we move the guard outwards to wrap the entire order struct, the behavior is spread out and much harder to understand -- particularly if the two conditions are merged into one:
224+
225+
```rust
226+
// The same match statement using or-of-guards.
227+
match order {
228+
Order {
229+
customer,
230+
payment: Payment::Cash(amount),
231+
item_type: ItemType::A,
232+
// A bunch of other conditions...
233+
} if customer.subscription_plan() == Plan::Premium && amount.in_usd() > 100 => { /* ... */ }
234+
// Other similar branches...
235+
}
236+
```
237+
238+
### Pattern Macros
239+
240+
If guards can only appear immediately within or-patterns, then either
241+
242+
- pattern macros can emit guards at the top-level, in which case they can only be called immediately within or-patterns without risking breakage if the macro definition changes (even to another valid pattern!); or
243+
- pattern macros cannot emit guards at the top-level, forcing macro authors to use terrible workarounds like `(Some(x) if guard(x)) | (Some(x) if false)` if they want to use the feature.
244+
245+
This can also be seen as a special case of the previous argument, as pattern macros fundamentally assume that patterns can be built out of composable, local pieces.
246+
247+
## Deref and Const Patterns Must Be Pure, But Not Guards
248+
249+
It may seem odd that we explicitly require const patterns to use pure `PartialEq` implementations (and the upcoming [proposal](https://hackmd.io/4qDDMcvyQ-GDB089IPcHGg) for deref patterns to use pure `Deref` implementations), but allow arbitrary side effects in guards. The ultimate reason for this is that, unlike const patterns and the proposed deref patterns, guard patterns are always refutable.
250+
251+
Without the requirement of `StructuralPartialEq` we could write a `PartialEq` implementation which always returns `false`, resulting either in UB or a failure to ensure match exhaustiveness:
252+
253+
```rust
254+
const FALSE: EvilBool = EvilBool(false);
255+
const TRUE: EvilBool = EvilBool(true);
256+
257+
match EvilBool(false) {
258+
FALSE => {},
259+
TRUE => {},
260+
}
261+
```
262+
263+
And similarly, with an impure version of the proposed deref patterns, we could write a `Deref` impl which alternates between returning `true` or `false` to get UB:
264+
265+
```rust
266+
match EvilBox::new(false) {
267+
deref!(true) => {} // Here the `EvilBox` dereferences to `false`.
268+
deref!(false) => {} // And here to `true`.
269+
}
270+
```
271+
272+
However, this is not a problem with guard patterns because they already need an irrefutable alternative anyway.
273+
For example, we could rewrite the const pattern example with guard patterns as follows:
274+
275+
```rust
276+
match EvilBool(false) {
277+
x if x == FALSE => {},
278+
x if x == TRUE => {},
279+
}
280+
```
281+
282+
But this will always be a compilation error because the `match` statement is no longer assumed to be exhaustive.
283+
284+
# Prior art
285+
286+
[prior-art]: #prior-art
287+
288+
This feature has been implemented in the [Unison](https://www.unison-lang.org/docs/language-reference/guard-patterns/), [Wolfram](https://reference.wolfram.com/language/ref/Condition.html), and [E ](<https://en.wikipedia.org/wiki/E_(programming_language)>) languages.
289+
290+
Guard patterns are also very similar to Haskell's [view patterns](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/view_patterns.html), which are more powerful and closer to a hypothetical "`if let` pattern" than a guard pattern as this RFC proposes it.
291+
292+
# Unresolved questions
293+
294+
[unresolved-questions]: #unresolved-questions
295+
296+
## Allowing Mismatching Bindings When Possible
297+
298+
Ideally, users would be able to write something to the effect of
299+
300+
```rust
301+
match Some(0) {
302+
Some(x if x > 0) | None => {},
303+
_ => {}
304+
}
305+
```
306+
307+
This is also very useful for macros, because it allows
308+
309+
1. pattern macros to use guard patterns freely without introducing new bindings the user has to be aware of in order to use the pattern macro within a disjunction, and
310+
2. macro users to pass guard patterns to macros freely, even if the macro uses the pattern within a disjunction.
311+
312+
As mentioned above, this case is not covered by this RFC, because `x` would need to be bound in both cases of the disjunction.
313+
314+
### Possible Design
315+
316+
[@tmandry proposed](https://github.com/rust-lang/rfcs/pull/3637#issuecomment-2307839511) amending the rules for how names can be bound in patterns to the following:
317+
318+
1. Unchanged: If a name is bound in any part of a pattern, it shadows existing definitions of the name.
319+
2. Unchanged: If a name bound by a pattern is used in the body, it must be defined in every part of a disjunction and be the same type in each.
320+
3. Removed: ~~Bindings introduced in one branch of a disjunction must be introduced in all branches.~~
321+
4. Added: If a name is bound in multiple parts of a disjunction, it must be bound to the same type in every part. (Enforced today by the combination of 2 and 3.)
322+
323+
## How to Refer to Guard Patterns
324+
325+
Some possibilities:
326+
327+
- "Guard pattern" will likely be most intuitive to users already familiar with match arm guards. Most likely, this includes anyone reading this, which is why this RFC uses that term.
328+
- "`if`-pattern" agrees with the naming of or-patterns, and obviously matches the syntax well. This is probably the most intuitive name for new users learning the feature.
329+
- Some other possibilities: "condition/conditioned pattern," "refinement/refined pattern," "restriction/restricted pattern," or "predicate/predicated pattern."
330+
331+
[future-possibilities]: #future-possibilities
332+
333+
# Future Possibilities
334+
335+
## Allowing `if let`
336+
337+
Users expect to be able to write `if let` where they can write `if`. Allowing this in guard patterns would make them significantly more powerful, but also more complex.
338+
339+
One way to think about this is that patterns serve two functions:
340+
341+
1. Refinement: refutable patterns only match some subset of a type's values.
342+
2. Destructuring: patterns use the structure common to values of that subset to extract data.
343+
344+
Guard patterns as described here provide _arbitrary refinement_. That is, guard patterns can match based on whether any arbitrary expression evaluates to true.
345+
346+
Allowing `if let` allows not just arbitrary refinement, but also _arbitrary destructuring_. The value(s) bound by an `if let` pattern can depend on the value of an arbitrary expression.

0 commit comments

Comments
 (0)