-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Guard Patterns #3637
base: master
Are you sure you want to change the base?
Guard Patterns #3637
Conversation
the keyword macro_rules! check_pat {
($p:pat) => { 1 };
($p:pat if $e:expr) => { 2 };
}
fn main() {
assert_eq!(check_pat!(Some(3) | None), 1);
assert_eq!(check_pat!(Some(e) if e == 3), 2);
// assert_eq!(check_pat!((Some(e) if e == 3)), 1); // <- workaround for existing editions
} this means at minimum this RFC will require a new Edition, or require that |
|
||
The only bindings available to guard conditions are | ||
- bindings from the scope containing the pattern match, if any; and | ||
- bindings introduced by identifier patterns _within_ the guard pattern. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with #2294 (rust-lang/rust#51114) the guard can be an if let
clause that introduces binding to the match arm:
#![feature(if_let_guard)]
fn main() {
match Some(Some(4)) {
Some(a) if let Some(b) = a => assert_eq!(b, 4),
_ => unreachable!(),
}
}
how will if-let guard interact with this RFC?
// error?
(Some(a) if let Some(b) = a.foo()) => using(b),
// error?
Some(a if let Some(b) = a.foo()) => using(b),
// error? assuming the `b` on both branches have the same type
(Some(Some(a)) if let Ok(b) = a.foo()) | (Some(Some(a)) if let Err(b) = a.bar()) => using(b),
// definitely error
(Some(a) if let Some(b) = a.foo()) | None => using(b),
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I wasn't aware of this feature. If you look in the Future Possibilities section I do mention the possibility of allowing if let
, but I hadn't realized it would be necessary for this RFC. I think probably all three of those patterns should be allowed, but to be cautious it might be better to restrict it to work only on the top-level for now.
_ => { | ||
// the user doesn't have enough credit, return an error message | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same example using is
expressions:
if plan is Plan::Regular && credit >= 100 || plan is Plan::Premium && credit >= 80 {
// complete the transaction
} else {
// return an error
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's not the same, the example uses user.subscription_plan()
and user.credit()
here rather than plan
and credit
. this rewrite will evaluate plan
twice.
plus you don't even need is
expression here as there is no binding, matches!(plan, Plan::Regular)
is sufficient, and if Plan
derives PartialEq you could even use plan == Plan::Regular
.
True, forgot to check that. I think I'd prefer making |
I think a better plan is to make This means this feature can be stabilized without waiting for a new edition. This is just like what happened with |
This may be a bit vague, but running an arbitrary logic for conditions in patterns sort of goes against the idea of a "pattern". Pattern is a "leaf" element of a condition and if you need to follow up on it using executable logic, you either
(This mostly applies to nonexhaustive matching, but matches involving The I have encountered the specific issue of two match arms being the same but not mergeable, but not too often. match foo {
A(_want_to_name_this_binding_explicitly_for_exhaustiveness_even_if_it_is_not_used) => {
// same logic
}
B /* no equivalent binding here */ => {
// same logic
}
} |
if you need to merge the two branches you could just use a comment instead of a real name? match foo {
A(_ /* name this binding explicitly for exhaustiveness */) | B => {
// logic
}
} |
I've added a section explaining how we can handle this the same way it was handled for or-patterns. |
> _PatternNoTopGuard_ :\ | ||
> `|`<sup>?</sup> _PatternNoTopAlt_ ( `|` _PatternNoTopAlt_ )<sup>\*</sup> | ||
With `if let` and `while let` expressions now using `PatternNoTopGuard`. `let` statements and function parameters can continue to use `PatternNoTopAlt`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A for
expression also accepts a Pattern
, but the pattern is required to be irrefutable.
for Ok(x) | Err(x) in vec![Ok(5), Err(6), Ok(7), Err(8)] {
println!("{x}");
}
I think this be changed to PatternNoTopGuard too?
Expression | Irrefutable | Current grammar | This RFC |
---|---|---|---|
let (/else ) |
PatternNoTopAlt | PatternNoTopAlt | |
Function & closure parameter | ✓ | PatternNoTopAlt | PatternNoTopAlt |
match |
Pattern MatchArmGuard? | Pattern | |
if /while let |
Pattern | PatternNoTopGuard | |
for |
✓ | Pattern | PatternNoTopGuard |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They could be, but wouldn't it be better to give an error saying the pattern must be refutable rather than a parse error?
I think that allowing guards in nested patterns is a natural step after allowing Guards are already part of the match construct, and allowing it in inner patterns doesn't actually bring a new functionality, it's just syntax sugar. The compiler is presumably allowed to hoist the guards outside the patterns (if they guarantee the guard only runs once), like you would do manually today. (Outside match, adding an inner guard means just wrapping the whole thing inside an enclosing The only issue with guards for me is that it trips the exhaustiveness checking (the compiler doesn't know if two or more guards cover all possibilities, so you need to delete the last guard to make it happy), but that's already an issue for the current guard feature in match, and allowing inner guards don't make it any worse. (There is an analogous issue with bare But anyway I think that this language feature is just syntax sugar and should be approached as such. |
This feels like a direct counter to the proposal in #3573: instead of allowing patterns as conditions, allow conditions as patterns. And I'll be frank, for the reasons mentioned, I don't like this. Adding a condition to a pattern essentially means that the pattern isn't covered at all, and so, it's just adding the pattern and condition together as a single condition, with the minor difference that if the pattern was covered completely in a previous branch, it'd be noticed as dead code. So... I would much rather have this just as the ability to combine patterns in an if statement beforehand, rather than a way of making match arms more confusing. |
Would this allow disjoint sets of bindings? e.g. (A(x, y) if x == y) | B(x) => ... To me it seems intuitive that this would be allowed (as it would now have a reason to exist, whereas before there would be no reason to bind a variable in only one branch of an or-pattern). Trying to access |
|
[drawbacks]: #drawbacks | ||
|
||
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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another drawback: patterns may now have arbitrary side-effects by evaluating expressions. Currently, no kind of pattern executes arbitrary code, and therefore in the current language version, unsafe code could rely on either using a caller-supplied $p:pat
inside a macro, or using a pattern containing a pattern macro from another crate, while a safety invariant is broken but unobservable.
This is probably fine as a change because no guarantee has been written, but it rules out deciding later that patterns won't ever have side-effects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in current language versions, unsafe code can not rely on user-supplied patterns not executing arbitrary code: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=1f3c26c1c0ae0732148ad5987321d2ce
matching against a const
may call PartialEq::eq
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@programmerjake your sample code generated the indirect_structural_match
lint, suggesting this is not intended to be compilable:
warning: to use a constant of type `Vec<u8>` in a pattern, `Vec<u8>` must be annotated with `#[derive(PartialEq)]`
--> src/main.rs:17:9
|
17 | C => {}
| ^
|
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #120362 <https://github.com/rust-lang/rust/issues/120362>
= note: the traits must be derived, manual `impl`s are not sufficient
= note: see https://doc.rust-lang.org/stable/std/marker/trait.StructuralPartialEq.html for details
= note: `#[warn(indirect_structural_match)]` on by default
basically the manual impl StructuralPartialEq for S
violated the definition of structural equality by having a PartialEq that does not compare structurally (also AFAIK StructuralPartialEq
is perma-unstable).
Deref pattern rust-lang/rust#87121 is another place that user code may be called during pattern matching, but the hole is sealed by another perma-unstable DerefPure
marker trait.
This seems reasonable. I've wanted this a few times myself, and it's always inconvenient to work around. It looks like the issue of edition handling has been addressed, and the issue of Let's see if we have a consensus on this: @rfcbot merge |
Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
|
||
```rust | ||
// Not allowed: | ||
let x if guard(x) = foo() {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the syntax would be invalid even without the guard, it could make this ambiguous. It'd be perhaps better to demonstrate otherwise-valid syntax, e.g.:
let x if guard(x) = foo() {} | |
let x if guard(x) = foo() else { loop {} }; |
while let x if guard(x) = foo() {} | ||
|
||
// Allowed: | ||
let (x if guard(x)) = foo() {} // Note that this would still error after parsing, since guard patterns are always refutable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it would be better to demonstrate a valid example, e.g.:
let (x if guard(x)) = foo() {} // Note that this would still error after parsing, since guard patterns are always refutable. | |
let (x if guard(x)) = foo() else { loop {} }; |
Rendered