-
Notifications
You must be signed in to change notification settings - Fork 207
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
[Patterns] Could we allow ==
patterns to have a receiver pattern?
#2620
Comments
Using relation patters as pattern suffixes instead of stand-alone can work. I worry that someone will start piling the operators up, so we get But then, if it's not a stand-alone pattern, we can allow both a prefix and a suffix versions, so you can write: case 0 <= var x < 10: ... You can also write a lot of ugly things, like And we can get rid of the Looks promising :) But in retrospect (the day after), it makes parsing horrible because of constant patterns, where Then we're back to allowing Maybe we should restrict it to one relational operator, to avoid |
Ooh, I really like how this looks. In fact I like it so much that I would be tempted to drop the null-check pattern from the language entirely and just make users always write
Agreed. I think the extra
I would just prohibit this by just saying that these operators are non-associative in patterns just like they are in expressions. If someone really wants to pile them up they can always do
As appealing as this is, I don't want to go there because of the parsing difficulties Lasse mentioned above. |
I just learned about if (x?) { (if it worked there too, maybe I'd be more likely to use it in patterns, as it's then it wouldn't feel a little obscure) |
Really cool idea. I like how it looks. I like that using if (_contiguousFunctions(arguments) case (var start, var end)?) {
if (_contiguousFunctions(arguments) case (var start, var end) != null) {
if (_blockToken(argument) case var bracket?) blocks[argument] = bracket;
if (_blockToken(argument) case var bracket != null) blocks[argument] = bracket;
case var block?:
case var block != null:
case MethodInvocation(:var target?) =>
case MethodInvocation(:var target != null) =>
if (block.argument case argument?) {
if (block.argument case var argument != null) {
// Or:
if (block case Block(:var argument != null)) { I think all of the I definitely don't think relational patterns should be associative. No chaining. Dart already prohibits chaining comparison expressions, and I can't see any value in allowing chaining Range and comparisonsIf we require a left operand on the relational pattern, then range and comparison patterns get more verbose: switch (n) {
case _ < 0: print('negative');
case _ >= 0 & _ <= 100: print('small');
case _ > 100: print('big');
}
// Versus:
switch (n) {
case < 0: print('negative');
case >= 0 & <= 100: print('small');
case > 100: print('big');
} I'm on the fence with this. I like the simplicity and clarity of always requiring a LHS, but it feels weird to require a bunch of essentially pointless Resolved type for the operatorHere's an interesting example: Object foo();
switch (foo()) {
case int _ < 3: print('A number less than three.');
} This is a compile error if we call Specifying that is a little interesting. We could say that we invoke the operator method using the required type of the LHS pattern, which means defining required type for all patterns, even ones that don't themselves type check. (Currently, we only define the required type for patterns that need it themselves.) Or we could rely on something like type promotion. We'd say the semantics of Expanding required type doesn't quite work because the required type of null-assert and cast patterns needs to be loose to allow them to be used in pattern variable declarations. Probably the easiest way is to just specify the "demonstrated type" for each kind of pattern (which will be the required type for most, GLB for Evaluation orderPattern matching can have side effects, so the evaluation order is visible. I think we probably want to evaluate the LHS pattern first before calling the operator. That's the order you get for expression evaluation where operands are (by necessity!) evaluated first, and it's what you see reading right to left. Unary patternsWe may want to allow a unary pattern on the left for things like: List<int?> allowLeadingNulls = ...
switch (allowLeadingNulls) {
// If the first element is null, we know the second won't be:
case [0, _! < 3]: ...
} And: List<Object?> json = ...
switch (json) {
case ['update-age', _ as int < 0]:
throw FormatException('New age must be a positive number.');
} Those also require the relational operator to be called on the value after we know its type based on the LHS pattern. I don't see much need to support No unary patternsThe other option is to require the LHS pattern to be a // Instead of:
case [0, _! < 3]: ...
// Write:
case [0, (< 3)!]: ...
// Instead of:
case ['update-age', _ as int < 0]:
// Write:
case ['update-age', (< 0) as int]: In that case, I'd probably want to allow omitting the LHS. But I lean towards allowing unary patterns since Next stepsI'd like to get more feedback since this is a fairly large change, but I'm definitely interested. If it were up to me, I would probably:
|
The more I think about this, the less sure I actually understand the semantics around how types get inferred or promoted. Consider: int? maybeInt = 0;
switch (maybeInt) {
case var x != null: print('x is not null');
case int y > 0: print('$y is positive');
case var z! < 0: print('$z is negative');
} The first question is do we expect all of these cases to be valid? If so, the intent is that
For the second case, I would imagine the execution is like:
The third case is similar to the second. These are obviously different semantics. Which do we pick? I suspect the right answer is that we pick the first semantics (which is what Leaf proposed) and the other two cases here do not work. Instead, if you want to call a relational operator that isn't defined on the matched value's type, you have to explicitly get it promoted first, using something like: case int y & y > 0:
case _! & var z < 0: Another option is to not treat equality and comparison patterns uniformly. We could have Then for the comparison operators, we could continue to not allow a pattern on the LHS at all, which makes range patterns (which are the main use case) terse: case > 0 & < 100: print('within (0:100)'); |
My thinking is that probably the second two shouldn't work. We could potentially say that for For We could consider adding an instance check pattern: Personally, I mostly find the variants written using For So I think you would say that for
I've based the above on the static type of There's a further tweak we could maybe do when the static type of The For
I'm not sure I 100% have all that right, but I think something like that is where we might go with that? |
Thinking about this a bit more, I think that we can probably just boil this down to #2622, and treat this entirely as syntactic sugar. That is, given a matched value type of
The we say that I think this gives us what we want. On order of evaluation, you say:
The semantics I propose above doesn't correspond to this: you call the operator before matching the LHS pattern. I think this is actually what you want - it corresponds to "outside-in" evaluation, which is what all of the other patterns do. This is different from expression evaluation order, but it feels like the right thing to me. Some examples:
So I think the consistent (and right) thing to do for |
If we did this, the demonstrated type would actually "promote" things in a lot more cases than we currently do type promotion for
Is there a consistency argument for making demonstrated types behave similarly?
I'm ok with this as a way to think about it. My usual caveat applies, which is "please don't actually spec it this way, though, because the implementations will not desugar it before running type analysis, so if we spec it in terms of a desugaring, we place a big cognitive burden on the implementers to "re-sugar" the spec while doing the implementation; and that leads to mistakes." 😃
Agreed. |
Looking at That makes sense, because So, we should not treat If we don't like doing the |
I don't follow you here. There is no Am I missing something? I think you can say |
My bad, there shouldn't be a The examples I would want to work are: case (nullableInt) {
case int x > 0: // x is non-negative int
case var x != null: // x is int If the matchee has type If we can't do |
@lrhn yes, that's a good example. I think this is perhaps pointing at the fact that there isn't a general instance check pattern (other than Object patterns). The only instance checks are either Object patterns or variable bindings. You could imagine have a general instance check pattern, e.g. I don't know that I'm strongly opposed to evaluating |
Generalizing It also means we have both prefix and suffix operators, so we need precedence. Previously we only had suffix operators. I think making prefix operators always bind stronger than suffix operators might work, so If |
Reflecting back on this after some time away from it, I think what I would personally favor is:
This means that if you want to match a variable and range check it you have to use some extra characters (e.g. I'm a little sad that this means that equality operators and relational operators aren't treated consistently, but I think it's justified because their use cases are so different. |
Would there ever be a reason to do When you've already recognized that the value Which means that And still not a lot of sense when So maybe it really is just (So, TL;DR, remove |
I could be talked into this. It has the advantage that it's way less implemetnation work 😃 |
Lasse and I spent a bunch of time talking about this today. As Lasse states elsewhere in this issue, it's not generally useful to allow a receiver pattern before The other more limited suggestion here is to change the null-check pattern's syntax from a postfix case var foo != null! is int: ... But there is still weird things like: case (var foo is int) != null: Overall, I feel like If we do something like Unless anyone strongly objects, I'll close this issue in a day or two and call it resolved. |
OK, closing this. |
The current proposal for patterns has a pattern of the form
== c
for a constantc
, and a derived pattern!= c
. The semantics of these are the same as the usualv == c
andv != c
expressions, where the receiver of the equality method call in each case is the implicit matched value of the pattern.This pattern does not allow matching further on the matched value. Could we either allow (optionally) or require a receiver pattern? That is, we could have
p == c
which is essentially equivalent to== c && p
andp != c
which is essentially equivalent to!= c && p
?This is nice syntactic sugar in general, but also allows for an (arguably) more readable
if case
syntax for checking for null:If combined with #2563 , then for explicit field reads you could replace
if (foo.bar case var bar?)
withif (foo case (:var bar != null))
which might arguably be more readable and more concise (whenbar
is a long name)?cc @mit-mit @munificent @lrhn @eernstg @kallentu @stereotype441 @jakemac53 @natebosch
The text was updated successfully, but these errors were encountered: