Skip to content
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

Boolean pattern matches #3062

Open
nex3 opened this issue May 11, 2023 · 7 comments
Open

Boolean pattern matches #3062

nex3 opened this issue May 11, 2023 · 7 comments
Labels
feature Proposed language feature that solves one or more problems patterns Issues related to pattern matching.

Comments

@nex3
Copy link
Member

nex3 commented May 11, 2023

As part of converting Dart Sass to Dart 3 style, I've noticed several cases where I wish I could use pattern matching just to express boolean conditions without any additional variable bindings. For example, I'd like to be able to write something like

this.node matches ListExpression(
  separator: ListSeparator.comma,
  hasBrackets: false,
  contents: [_, _, ...]
)

instead of

var node = this.node;
node is ListExpression &&
    node.separator == ListSeparator.comma &&
    !node.hasBrackets &&
    node.contents.length > 1

...but there's not a great way to do that today. The best alternative is something much more awkward like

switch (this.node) {
    ListExpression(
        separator: ListSeparator.comma,
        hasBrackets: false,
        contents: [_, _, ...]
    ) => true,
    _ => false
}

Edit: I did originally say "without any additional variable bindings", but I'm also running into cases where it would be useful to have a when clause that can access bound variables.

@nex3 nex3 added the feature Proposed language feature that solves one or more problems label May 11, 2023
@munificent
Copy link
Member

munificent commented May 11, 2023

Yes, this is a cool idea. The obvious syntax would probably be <expr> case <pattern>, like the head of an if-case statement:

var isNonEmptyList = this.node case ListExpression(
        separator: ListSeparator.comma,
        hasBrackets: false,
        contents: [_, _, ...]
    );

The hard part is figuring out the precedence and determining when the pattern on the right is done and any surrounding expression begins. The expression and pattern grammars are both quite complex, so the current design works pretty hard to keep them clearly partitioned from each other so that the parser (and reader) can always tell when one ends and the next begins.

If we limited the pattern after case to be an explicitly delimited one (record, list, object, map, or parentheses), that would probably do it.

We would probably also have to restrict it by saying that any pattern used in this kind of expression can't bind variables because otherwise the scoping of those variables gets... weird.

@munificent munificent added the patterns Issues related to pattern matching. label May 11, 2023
@lrhn
Copy link
Member

lrhn commented May 15, 2023

Binding variables is fine, but the when clause would be the only place they are in scope.
Except if you use it in a condition where code is guarded by the test. This would/should allow:

if ((e1 case int x) && (e2 case int y)) { return x + y; }

Promotion due to pattern matching would still work through the boolean variablen, like we do for other type checks today.

I'm not too worried about delimiting the syntax, if we can just require parentheses in most contexts where there could be any doubt.

@nex3
Copy link
Member Author

nex3 commented May 25, 2023

As an additional note: if this is added, it would be nice to also have !case or similar syntax for the negation, to avoid the awkward !(name case ... ) form.

@HENRDS
Copy link

HENRDS commented May 25, 2023

Edit: I just now noticed that Dart already has the if-case syntax so using is would be inconsistent. Please ignore the comment.

I don't know about the feasibility of it in Dart, or if it has already been proposed, but a syntax similar to C#'s could be an option.

It would basically consist of allowing patterns after is and is!:

if (this.node is ListExpression(separator: ListSeparator.comma, hasBrackets: false, contents: [_, _, ...]))
{
    // ...
}


if (name is! String) {
    // ...
}

Binding the result to a new variable could simulate when:

if (x is Foo(bar: Bar.optionA) foo && (foo.baz == 1 || foo.baz == 2)) {
    // ...
}

@amirburbea
Copy link

I've also noticed that not having these as expressions means I can write

if(list.last case MyType m) {
   doSomethingWith(m);
}

but I can't write a expression where the pattern is not the primary boolean condition

if(list.isNotEmpty && list.last case MyType m) {
}

Yes I know there's a .lastOrNull extension but that is a specific example. This scenario works easily in C# pattern matching.

@munificent
Copy link
Member

@HENRDS, yes, we discussed using is extensively. If you poked around in the closed issues in the issue tracker you might be able to dig it up. The main problem is that Dart has type literals so is int already means one thing and case int: already means something very different. Extending is to allow a pattern on the right would either break almost all existing is expressions or require a weird irregularity in what the RHS of is means. Using case avoids all that.

@amirburbea, I've run into this too, a number of times. In some cases, you can use a when clause for the conditional part. But, if, as in your case, the condition needs to be checked first and short circuit or the pattern with throw, then it's harder. Note that in your specific example, you could do:

if (list case [..., MyType m]) {

@HENRDS
Copy link

HENRDS commented Aug 29, 2023

@munificent, that makes sense, thanks for explaining.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems patterns Issues related to pattern matching.
Projects
None yet
Development

No branches or pull requests

5 participants