-
Notifications
You must be signed in to change notification settings - Fork 205
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
Pattern matching as an alternative to if-variables #1977
Comments
I really like this approach. When I was first working on patterns, I spent some time trying to come up with a nice syntax for a single-pattern I see the proposal you have here as three subcomponents:
For what it's worth, the pattern matching proposal does already define syntax for the last two. If we were to use those, you get: if (var (length: length) if (length > 0) = list) {
print("Non-empty list of length $length");
} Guard syntaxI think using Field destructuringMost languages that have some kind of record-like destructuring have sugar for binding a variable with the same name as the field. For example, JavaScript allows both: var {a: x, b: y} = {a: 1, b: 2}; // x = 1, y = 2.
var {a, b} = {a: 1, b: 2}; // a = 1, b = 2. Rust allows both: let Point { x, y } = p;
let Point { x: a, y: b } = p; I didn't propose syntax for this for Dart yet. It's harder for us because we use Another nice thing about if (var (name: name, phone: (ext: ext, digits: digits))) { ... } Or with some sugar for cases where the subpattern is a variable with the same name as the field: if (var (name:, phone: (ext:, digits:))) { ... } Matching
|
I don't think In general, I need more separation and grouping to be able to easily read these run-on expressions. As for pattern syntax, I'd prefer something more like C#'s object matchers: if (_ is List{length: var length} list && length > 0) ... That pattern (We can probably abbreviate Introducing arbitrary if (_ is List{length: if it > 0} list) ... (using implicit |
The braces don't do much for me aesthetically. Note that because Dart has map literals (unlike C#), it's harder to use those since they can collide with map patterns. Even if they don't technically collide in the grammar because of the leading type name, they may look confusing to a reader who expects In general, pattern syntax mirrors the syntax used to create the object being matched. For instances of classes, that's constructor calls, which is why I used parentheses. The proposal already has "extractor" patterns which let you combine type testing and destructuring. They look like: List(length: var length) In the context of a matching-if statement, it would be something like: if (var List(length: var length) where length > 0 = list) ... |
The Parentheses in the extractor pattern are good too, I was considering those as the next option if braces won't work. It does look like a constructor - but it isn't related to constructors if all it does getter accesses, which is what worries me a little. Anyway, then it can be written as: if (list is List(length: var length) && length > 0) ... without the if (this.list is List(length: var length) list && length > 0) ... if you want to bind a value which cannot be promoted. if (this.list is List(length: var length if (length > 0)) list) ... (Using a keyword to make it clear where the condition starts, and parentheses to make it work inside an expression: if (this.list is List(length: var length) list if (length > 0) && somethingUnrelated) ... Not sure |
I don't think I prefer either of those. :) My preference would maybe be: if (list case List(length: var length) where (length > 0)) ...
So the thing after
Yes, pattern matching is mostly syntactic sugar. However:
|
I don't hate the
My syntax is definitely more limited (it can only handle a single field per object) but it does nest. Example: if (var _.phone.ext where ext != null) {
print("$phone is in scope here");
print("$ext is also in scope here");
} It's possible we could include
This really doesn't work to me.
This works ok for me. I do wonder whether having the |
Agreed, this felt like it got lost.
As a data point, it took me a long time to figure out what (I think) this is supposed to mean. I think I don't like having to write I also really dislike having to repeat the field name as the variable name. The key piece of doing anything here is to be able to pun on the field name to get the variable name. Otherwise I don't see much point. |
I can definitely see a use for refutable patterns on ordinary variable bindings as well, where you have invariants that can't be captured in the type system that establish properties that you just want to assert, in much the same way that |
The
I'd expect a pattern to be able to match values, so: if (list is List{length: 8}) ... checks whether the list has length 8 (it checks the value of the var expectedLength = 8;
if (list is List{length: expectedLength}) ... and use a variable as the variable pattern. If that meant "bind the value to the new variable That means that if I want to bind at that position, I need to write something more, which is why I add if (list is List{length: var length}) ... Here Also, I like having I'd be fine with allowing |
Wait, what? Then what is being discriminated on? There are no other candidates to be expressions in that syntax. |
This is not a general pattern matching proposal, this is a proposal for a specific small syntax that might be a subset of a pattern matching proposal (if we do one).
I'm not very keen on this. Patterns aren't expressions, and it gets weird if you allow them to be.
This is a good reason not to allow expressions there. :)
Right, this is provided by the |
I admit that C# does not allow general expressions, only constants, but they do allow named constants, so const int expectedLength = 8; // assuming you can declare a local const in C#
if (something is List{Length: expectedLength) list) { ... } matches lists with length 8. They have relational patterns too That does mean that they have to introduce new syntax to combine patterns ( const int expectedLength = 8; // assuming you can declare a local const in C#
if (something is List{Length: > 4 and < 8) list) { ... } rather than use normal expression syntax (say I'm not sure why allowing general expressions inside patterns is bad. They contain values, and the way to express values is expressions. I can see why making some values necessarily constant can allow some optimizations, like; case < 4: ...
case >= 4 && < 8: ...
case >= 8: .... can be recognized as distinct ranges and optimized in some numerical way. |
One reason is grammatical ambiguity. Consider an imaginary syntax: Another reason is, as you mention, efficiency of compilation. For complex patterns, languages that rely on pattern matching use various strategies to pick the order in which to do tests. For a simple example, given this code:
You'd like to compile the complex match to a simple match of the form:
This avoid doing the complex, expensive destructuring of Standard ML of NJ used this approach, I think, for building decision trees from patterns. Lennart Augustsson had a paper on doing this for lazy languages where left to right order had to be preserved (since termination is an observable side effect). |
Taking up grammar space and being ambiguous is a good argument. As for using constants to do extra-efficient compilation, that's a fine feature, but it doesn't prevent you from doing non-constant values too. That would have to have a specific evaluation order, which makes it harder to optimize, but you still get exactly what you ask for. Something like: case < start: ...
case >= start && < end: ...
case > end: ... where I also like The last one is the known pattern for doing binding type checks, I'm a little worried about inventing too much new syntax just for patterns, like |
I hear you. If we were designing Dart from scratch, I'd probably go in that direction too. But I think we are already in a position where curly braces mean "map"—a homogeneous data structure where all key-value pairs have the same type. If you want a thing that exposes named properties whose individual types are known to the type system... that's kind of what an instance of a class is. And the way you construct instances of classes is The mental model I have is:
I look at tuples and records as basically just sugar for classes. And, in particular, I want to allow destructuring pattern matching on instances of user-defined classes so that algebraic datatype-style code is easy to write. The syntax for that in most languages I know is It also nicely allows mixing positional and named destructuring, which mirrors function calls in Dart which can mix positional and named arguments: var call = foo(1, 2, named: 3);
var construct = Foo(1, 2, named: 3);
var record = (1, 2, named: 3);
var (a, b, named: c) = ... // Pattern.
Yeah, in SML in friends you have switch (expr) { case pattern: ... }
// expr case pattern So I don't think it would be too crazy to do |
So returning to your original counter-proposal from this thread then, how does this match up?
The above looks like it's using the record syntax. Give what you say above, I'd expect something like: // Patterns proposal with "if ... case" and "where" for guards:
if (list case List(length:) where length > 0) { ... } That feels like it is getting a bit verbose though. Is your original counter-proposal assuming some inference? I think it might be doable, but there's definitely a lot of room for ambiguity to creep in. |
The way to think about it is that every pattern syntax is essentially sugar for accessing a certain protocol, not just matching over a specific concrete type. So:
In the proposal, the protocol for named field access in record patterns is literally just "the type has a getter with the corresponding name", so you can use
You can do this too, though it's redundant here since we know the matched value
Yes, the patterns proposal specifies how to take the static type of the matched value and use that to infer a type for the pattern. It's similar to how upwards inference works for local variables. (You can of course think of local variables as simply a non-refutable pattern match of a simple variable pattern to the initializer's value.) I'm definitely not an expert at this stuff, but I did my best to work it out in some detail. I think type inference here is necessary for usability. For example: var tuple = (1, 'a string');
var (a, b) = tuple; Users would be unpleasantly surprised if |
Ok, this makes sense.
Sorry, I wasn't specific enough. I'd assumed that there would be inference to infer the types of the bound variables from the pattern, but I was wondering more about how |
Out of all of the proposals to address non-null promotion of fields (stable getters, shadow) and the various if-variables / guard proposals, I feel like pattern matching has the least noise, and is more compatible with the features that users would like to see in dart in the near future (patterns & related features, data classes, destructuring, tuples)... I would rather have the language move this way, where the changes all seem like a unified flow, rather than a bunch of random syntax noise like stable getters / shadow vars / let vars / naming subexpressions using '@'. It would also keep dart easily able to learn / pick up with more people coming from modern languages that have pattern related features where they have proved their usefulness and clarity, rather than forcing people to learn about oddities around stable getters, etc (sorry to pick on stable getters). It also seems like this discussion has converged more easily to a point of view where most people agree it is worth doing with only slight variation of opinion on syntax. I feel like @munificent's proposal on patterns addresses how patterns might differ in dart as compared to other languages and the reasons, and I feel like it has very sound arguments in favor of doing it with the syntax he proposes. |
I agree. I feel like there's a lot of fragmented discussion on what's the best sugar for a few specific cases, but that's just going to end up with a language with a lot of very niche corners. One feature that covers them all -- and leaves room for expansion -- would be a real step up. (Also for the record I'd still like to see stable getters because it's not just sugar for if-statements, it's an API design feature that will help with making dataclasses more immutable, allowing more expressions to be |
Valid point, while pattern matching is generally useful in many cases for if-vars + more complex things, it does not make the null-check common case short. However, for your example I would argue we don't need if-vars at all. The real value of this proposal is in the destructuring of regular dart objects using getters (you can get multiple fields into a local var) and guarding against conditions in an if statement. |
We've shipped patterns now and with But there is something there. Do we want to keep this issue open? |
Closing, thanks! |
In this issue, I proposed an alternative syntax for the if-variable feature described in #1201 . Discussion in #1975 suggests that the two alternative syntax proposal have problems on orthogonal axes. In the original syntax, it is not obvious where variables are being defined but in so far as one correctly understands where variables are bound, it is clear what they are bound to; and in the alternative syntax, it is obvious where variables are being defined, but it is not necessarily obvious to what they are bound. This proposal explores using a pattern matching syntax as a solution to make it clear(er) both where variables are bound, and to what they are bound.
This isn't a full syntactic proposal: this is just intended to explore whether a path forward might lie via the larger feature of pattern matching that we have discussed separately. For the purposes of this exploration, let's assume the ability to write a pattern of the form
var _.<identifier> <whereclause>? = <expression>
(and similarly forfinal
) whereidentifier
is an identifier name,expression
is a standard Dart expression, andwhereclause
is of the formwhere <expression>
whereexpression
is a boolean valued expression. The variableidentifier
is bound in the where clause.A pattern with a where clause is refutable, and is considered not to match if the boolean expression evaluates to false. A refutable pattern may be used as the condition in an
if
statement: if the pattern is not refuted theif
branch is evaluated, and all of the variables bound by the pattern are in scope in theif
statement. Otherwise, the failure branch is taken (i.e. either theelse
clause or the next statement of the enclosing block).As a syntactic shorthand, we might consider allowing the form
Type _.identifier = expression
to stand forvar _.identifier where identifier is Type = expression
.As another syntactic shorthand, we might consider allowing the form
var! _.identifier = expression
to stand forvar _.identifier where identifier != null = expression
.Example:
We might also consider allowing negative conditions to bind variables in the continuation as discussed in the original if-variable proposal, but I'm not sure it's clear what the set of
negative
conditions would be. As an alternative, we could add a guard syntax which binds in the continuation and requires the body of theif
to throw, e.g.:Simple Example
Continuing by example, using the examples from the previous proposals:
Promoting on null checks
Promoting on getters
Negative if-vars
Worked example
New syntax for the worked example from here:
New syntax from the worked example from here.
cc @munificent @lrhn @eernstg @jakemac53 @natebosch @stereotype441 @mit-mit
The text was updated successfully, but these errors were encountered: