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

Why can't I write a guard clause for each pattern in the guard clause of pattern matching? #4094

Open
yukisakai1225 opened this issue Sep 16, 2024 · 6 comments
Labels
request Requests to resolve a particular developer problem

Comments

@yukisakai1225
Copy link

In the following code, “B” is assigned to res.
This is because the guard clause is evaluated after pattern matching in line 3.

final (a, b) = (0, 0);
final res = switch(a) {
  == 0 || _ when b == 1 => "A",
  _ => "B",
};

I expected the guard clause to affect each pattern as follows.

final (a, b) = (0, 0);
final res = switch(a) {
  == 0 || (_ when b == 1) => "A",
  _ => "B",
};

Consider other languages.
In Swift, we can write guard clauses for each pattern, but in Rust and Scala, we cannot write guard clauses for each pattern.
I would like to know the background behind this kind of specification for Dart.

I am not good at English, so sorry if my writing is not clear.

@yukisakai1225 yukisakai1225 added the request Requests to resolve a particular developer problem label Sep 16, 2024
@hydro63
Copy link

hydro63 commented Sep 16, 2024

From what you are saying, you want to be able to use when also in subpatterns, right? Something like this?

var dummy = rnd.nextDouble();
x = switch(obj){
  (Foo() || (Bar(b: var b) when b > 10) || (Baz(c: var c) when c == 0)) when dummy > 0.5 => ...
  ...
}
// aka having multiple when clauses in patterns, each one for it's own subpattern

If that's what you want, i believe the reason is because it makes it more difficult to read what is actually matching instead of just having one when, where you can put all the additional checks. Another reason is that it makes patterns somewhat more difficult to compose, since one inner when clause can alter the execution of the entire pattern.

There is one issue that has been talking about this (#4057), but only very briefly. It also proposes allowing functions in pattern, which would effectively be able to do the more complex subpattern checking, that you (probably) want.

@mateusfccp
Copy link
Contributor

I know this is what you are asking for, but sometimes you can rethink how you are going to express your code.

For instance, the above could be written as

final r = (0, 0);
final res = switch (r) {
  (0, _) || (_, 1) => "A",
  _ => "B",
};

Or

final (a, b) = (0, 0);
final res = switch(a) {
  0 => "A",
  _ when b == 1 => "A",
  _ => "B",
};

Either way are more readable IMO, so you may want to use them, as currently it's impossible to do what you want with the syntax you want.

@yukisakai1225
Copy link
Author

From what you are saying, you want to be able to use when also in subpatterns, right? Something like this?

You are right. Thank you for taking my intentions into consideration.

As you say, Separating the pattern and guard clauses would dramatically change the readability of the implementation regarding pattern matching.

@yukisakai1225
Copy link
Author

@mateusfccp
I want to separate a line for each value returned after the pattern match.
So, this sentence is a little less than my favorite.

final (a, b) = (0, 0);
final res = switch(a) {
  0 => "A",
  _ when b == 1 => "A",
  _ => "B",
};

I prefer the first example you presented.

final r = (0, 0);
final res = switch (r) {
  (0, _) || (_, 1) => "A",
  _ => "B",
};

If we want to add a guard clause to either (0, ) or (, 1), we need to separate the lines, as follow.
I am looking for a way to avoid this.

final r = (0, 0);
final res = switch (r) {
  (0, _)  => "A",
  (_, 1) when xxxx => "A",
  _ => "B",
};

@lrhn
Copy link
Member

lrhn commented Sep 18, 2024

There's is the option of not using a switch expression, but a switch statement, where each body can have more than one case:

final r = (0, 0);
final String res;
switch (r) {
  case (0, _):
  case (_, 1) when xxxx:
    res = "A";
  case _: 
    res = "B";
}

It has only one line for each value signed to res, and individual when clauses for each pattern.

@munificent
Copy link
Member

I would like to know the background behind this kind of specification for Dart.

As far as background goes... adding pattern matching to a language that wasn't originally designed to have it is really hard. Dart already had switch statements, variable declarations, parameter lists, and lots of other features that touch on patterns before it had pattern matching.

When we added patterns, we wanted them to feel as natural in the language as possible but also had to avoid breaking changes to existing language features as much as possible. We also had to try to make Dart's pattern matching features look familiar to users coming from other languages with similar features.

All of that put a lot of constraints on the syntax design. With switch statements, the language already supported having multiple cases share a body. That made it natural to also allow multiple cases to share a body when those cases may have patterns and guards too. So in switch statements, it felt pretty reasonable to make the guard part of the case and not part of the pattern itself. The downside is that you can't have guards nested inside a case. But since you rarely need that and can usually just have multiple cases with a single body, it's rarely a limitation in practice.

With switch expressions, it was hard to design a syntax that we felt looked good. And we never came up with one that looked right while allowing multiple cases to share a body. A consequence of that, as you note, is that that also means you can't have multiple guards for the same body in a switch expression.

We could have tried to make guards part of the pattern itself instead of part of the surrounding construct (switch case, if case, etc.). That's not a bad idea, but the grammar can get tricky. A guard can have any arbitrary expression. But patterns like || and && also allow an arbitrary pattern on the right-hand side without any closing delimiter. So when a guard is nested inside a pattern, it might be hard for the grammar to tell when the guard ends and the pattern begins. Consider:

switch (obj) {
  case a when b || c: ...
}

Should that be parsed like:

  case (a when b) || c: ...

Or:

  case a when (b || c): ...

We probably could figure out an answer to this (I imagine we'd make guards the lowest precedence pattern form), but it can get confusing for users.

It's not a bad idea, though, and maybe one we should consider. It would be nice if you could put arbitrary predicates inside a pattern and this would provide a way to do that.

But the short answer to your question is that syntax design is hard and sometimes we do something simpler and less general at the expense of expressiveness if we feel the result is more usable or more feasible to design and ship in a reasonable amount of time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

5 participants