-
Notifications
You must be signed in to change notification settings - Fork 211
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
Allow patterns after is
.
#2137
Comments
(Allowing bindings in any expression does mean that we have effectively introduced a |
Java allows this as well: https://openjdk.java.net/jeps/394 Though I must admit I find this syntax a bit hard to read at times. I would be much more in favour of something that goes right to left because it reads as "pattern match to value", but I can't really come up with any reasonable syntax which does not look, ahm, strange. if (int value = this.field) {
} Though semantics becomes a bit confusing here, because it does not act like a normal assignment (which would throw if types don't match or refuse to compile). One thing we could consider here is that promotion to // given this.field is int?
if (this.field is var value) {
//
} though it looks rather cryptic... |
I kind of like this syntax when limited specifically to condition guards. I continue to be strongly opposed to having non syntactically delimited variable binding scopes for the reasons that I partially sketched out here . So allowing this in arbitrary |
From first blush, I like the way this looks. Familiarity with C# and Java is a plus. There are a couple of ambiguity problems, though. :( Identifiers as types or constantsif (value is int) { ... } Today, this is a type check. If we treat the RHS of For backwards compatibility, we could say that if the RHS is a (possibly qualified) identifier, then it continues to behave like a type check. But that means that the RHS isn't compositionally consistent: if (1 is int) print('ok');
if ((1, 2) is (int, int)) print('oops'); Here, the first We could say that an identifier in a matcher pattern is treated as a type check pattern (i.e. Or we could say that bare identifiers aren't treated as constants in matcher patterns and require some other syntax. But that breaks compatibility with existing switch cases. Null-check patterns versus nullable typesAnother ambiguity is: if (value is int?) { ... } Today, this is a type check against a nullable type. If we interpret
(Granted, this pattern is already confusing. We might want to add a notion of precedence to the pattern grammar so that the operand to a null-check pattern can't be a constant or literal pattern since that's unlikely to be useful.) Where it can be used and variable scopeI agree with Leaf that I don't want to allow function(value is int x, x);
receiver.method(value is int x).chained(x); Are these allowed? What fraction of Dart users would ever be able to intuit whatever answer we choose? I think the the safest/sanest approach is to not allow code like this. It just gets too weird. That raises the questions of what the actual boundaries are restrictions are. I could see some combination of:
|
If both Java and C# are doing it, have they solved the confusion problem? Or is it not really a problem the way people actually use the feature? (I'm sure you can do something obscure, but if you won't get it through code-review, it's not a real problem. Working well in idiomatic code, and being confusing if misused, that describes most powerful language features.) It seems that C# introduces the variable at the test (it's not a variable syntactically before) and it then treats it as a normal block-scoped variable which is uninitialized if the test fails. Object o = "A";
if (o is String a && o is String b) {
Console.WriteLine(a.Length + b.Length);
}
a = "b"; // Allowed
Console.WriteLine(a);
Boolean z = o is String y && y.Length == 0;
//Console.WriteLine(y); // unassigned variable y
String w = o is String v ? v : "not string";
Console.WriteLine(o is String u); Java seems to only have the variable directly downstream of the test, and special case boolean-based control flow to propagate the variable. Object o = "A";
if (o instanceof String a && o instanceof String b) {
System.out.println(a + b); // Works
}
Boolean x = (o instanceof String b) && b.length() == 0; // Works
// b not in scope here at all.
String v = o instanceof String z ? z : "not string"; // Works.
System.out.println(o instanceof String q); // Works In both cases, the variable introduced by the test is alive in the code dominated by a successful test, but no further than the current block. Have we ever heard of anyone finding either Java or C# confusing at this point? Or are users just automatically using the test in the most obvious way, which works pretty well, and not pushing its edge cases? About chaining tests, consider a situation like; if (e1 case String b && b.length > 0 && e2[b.length] case otherPattern) ... I expect such situations to happen, where you want to use a pattern match as part of a longer boolean expression, interleaved with other tests. Heck, I want |
Quick note: I found an excellent discussion from the C# folks about the approach they took for scoping variables declared in |
That's a good discussion of the scoping choices, but doesn't cover the discussion on making |
Are there any real use cases for this feature outside of conditionals? What if instead of making this an expression, we just defined an extended boolean expression grammar that can only be used conditionals (or possibly only in if case` conditionals) and includes these expressions. That is, suppose we define conditional patterns to be something like : What are the convincing use cases for including this into the expression grammar? And if there are use cases, is it enough to then provide an inclusion into expression grammar? For example, add |
If there are no use-cases outside of conditionals, then it might still be easier (implementation-wise) to allow the feature everywhere, than it is to introduce new syntactic categories that can only be used in a few cases. And that may or may not be complete enough for what users want to do.
If there are no other use-cases than conditionals, then people just won't use it in other ways, but if we make a special grammar for conditions, people will still need to learn that. I do want to support the conditional expression, for things like var result = field is Foo f ? f.getResult() : nonFooResult(); Conditions are not just if (field is Foo f && f.isOK) {
...
} but not while (field is Foo f && f.isOK) {
...
} (The Now, for something probably a bridge too far: @stereotype441, you were working on linking information to boolean variables. void main() {
Object o = [];
var b = o is List;
if (b) {
print(o.length); // This works!
}
} I'm not sure it's a good idea to allow: var b = o is List l;
// l not in scope
if (b) {
// l in scope as List here
}
// l not in scope On the other hand, it would be perfectly in-line with treating the scope of the variable with the scope where the same |
I'm not worried about the implementation cost, I'm worried about the user experience. I continue to strongly feel that introducing scopes in the middle of arbitrary expressions where the nesting depth is unrelated to the scope is a very poor idea, whether C# decided to do it or not. I'm perfectly comfortable with introducing scopes in the headers of |
I find this syntax really nice. I assume it cannot be made void fn(dynamic a) {
if (a is int a) {
a++;
}
// no bleeding scope
print(a);
}
fn(3); // prints 3 On the other hand having it final would not be consistent with the rest.
What is the use case / appeal for a "bleeding scope" ? This is unintuitive, inconsistent with the rest of the language and unconventional with how scopes usually work in other languages beside the c# example and maybe js var hoisting. If it bled I'd expect other parenthesis to bleed as well, which would be really unconventional. It also prevents the compiler from catching errors for (int i = 0; i < 5; i++) { }
print(i); // no this is a mistake caught by the compiler Maybe I misunderstood the bleeding part though as it seems out of place |
I think I'm with Lasse that it's probably more trouble than it's worth to create conditions as a separate syntactic category. I could see us maybe wanting to do that it we wanted to use the same syntax to mean different things in each category. (Trivia: I believe BCPL uses I haven't had a chance to write this up yet, but when Lasse and I were last talking about this, one idea was to add a // show a validation error if the JSON isn't the right format:
return TextFormValidator(isValid: json case ['user', String _]); |
I wrote up a strawman for the idea in my last comment here: #2181 |
I don't think I ever responded to this important question:
Java and C# don't have type literals and Dart does. That is, I think, the key reason why extending var x = 1;
if (x is int) print('type test');
// ^^^
switch (x) {
case int: print('equivalent to type literal');
// ^^^
break;
} So we already have two places in the language that want to generalize to allow patterns, but where existing valid syntax means something completely different. Java and C# avoid that because there are no type literals. I don't see how we can generalize both The only remaining idea I can think of is to eliminate type literals from the language (#2393). If we manage to do that (unlikely), then maybe we could revisit this. |
There are places where a type is just expected. If you write a single identifier, it's a type. If you write more, the rest may be a variable name. That's currently just new-style function types: We have a general problem with deciding what a plain identifier means in different pattern contexts, because we currently do different things in different places that should all be accepting patterns.
I suggested elsewhere that we could let patterns work differently in different contexts (with the option of forcing a context with a prefix), so:
With this, This can, possibly, allow us to generalize all these positions to accept patterns without changing the behavior of currently used syntax. |
dart-lang/language#2137 (comment) In particular: - Instead of if-var statements, use `is <pattern>` as the condition. - Require "var" before variables in cases.
I went ahead and tried to apply the contextual syntax you describe here to my test refactoring of dart_style to use patterns. It looks like this. My impression:
|
if (char is! 0x20 | 0x09 | 0x0a | 0x0d) I like code golf as much as the next geek, but this is a severe parsing ambiguity since Putting the pattern first does achieve this by delimiting it by Having more than one way to do things is what patterns are for. You can write For example, |
For what it's worth, the patterns proposal explicitly disallows that (since it's weird and unreadable) by only allowing a subset of patterns to appear as the outermost pattern in a pattern declaration context. |
I'm going to go ahead and close this because, which I wish we could make it work, I think type literals and some other syntactic hard/soft constraints make it too much of an uphill battle. |
The current pattern matching proposal special cases pattern matching in
if
statements.It uses a syntax which looks like assignment.
Consider instead allowing a pattern after any
is
check.or
Further, it allows chaining pattern matches as:
As precedence, C# already does this.
By allowing any matcher pattern after
is
, we also allow patterns to be used in conditional expression (?
/:
) and anif
-element (in a collection literal), not just in anif
statement. That does mean that we get local variables introduced inside those constructs, probably limited to the rest of that expression.It makes patterns a more "first class" language feature by allowing it orthogonally anywhere a test is allowed, not just in specific statement contexts.
The text was updated successfully, but these errors were encountered: