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

If-case expressions #3059

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

If-case expressions #3059

nex3 opened this issue May 11, 2023 · 11 comments
Labels
feature Proposed language feature that solves one or more problems feature-completeness A special or edge case of another feature which isn't supported patterns Issues related to pattern matching.

Comments

@nex3
Copy link
Member

nex3 commented May 11, 2023

(Sorry if this is already filed, I did do a search but GitHub's search is pretty rough for something with this broad a name.)

One hole I've run into when working on converting existing code to Dart 3 style is the absence of an expression-level "if-case". To put it another way, there's a gap in the following matrix:

Statement Expression
Two Cases if (foo case [a, b]) {} else {} ???
Many Cases switch (foo) {case [a, b]: /* ... */} switch (foo) {[a, b] => /* ... */}

I'd expect to either be able to use case with a ternary expression, or to be able to use the existing if-case syntax at the expression level (possibly without brackets to match the collection literal syntax), but neither of these works. I could use a switch expression here, but it involves more and more awkward nesting than if or ternary expression would. I think the shortest way to express this right now is actually:

[
  if (foo case [a, b])
    something(a, b)
  else
    somethingElse()
][0]

...but that's too silly to even consider.

(Addendum: pattern matching is a wonderful feature and I'm very excited to use it in Dart.)

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

incendial commented May 11, 2023

Feels like this issue #2537 has a discussion about this problem.

Edit: actually, if guards are expected to always diverge, then it's a different thing.

@munificent
Copy link
Member

You're right, this is a missing cell in the table. (The other missing one is switch collection elements.)

It's tricky because if-case is a riff on if. There are no if expressions either. Instead, that hole is filled with the syntactically unrelated ?: conditional expression. We could do an if-case like expression form that built on the if-case syntax, but then it would feel weird to not actually have if expressions.

Or we could try to build some weird pattern-based ?: expression, but I suspect that would look too alien. In principle it could just be:

var x = foo case [a, b] ? something(a, b) : somethingElse();

That's actually not horrible, but it feels really weird to double down on ?: which is already honestly not a great syntax.

@nex3
Copy link
Member Author

nex3 commented May 12, 2023

The first thing I tried before filing this issue was using case in a ternary expression, and I initially assumed that would work. I personally like the ternary format, but I would also be fine with just allowing if expressions everywhere. (Note that from a user's perspective, if expressions do exist in a collection context, just not anywhere else.)

@lrhn
Copy link
Member

lrhn commented May 12, 2023

We could make (expr case pattern when expr) ? expr : expr work. We'd probably have to require the case-clause to be parenthesized, but other than that, it shouldn't be a problem. Pattern bound variables would only be in scope in the "then" branch.

I'm not personally a fan of the ?/: conditional expression syntax. It's a waste of good symbols, and would be quite happy using if (expr) expr else expr as an expression instead.

In any case, for now a

(e1 case pattern when e2) ? e3 : e4

can be expressed as

switch (e1) { pattern when e2 => e3, _ => e4 }

It's somewhat longer. The ?/: has brevity going for it. No much else, but it is really short.

@nex3
Copy link
Member Author

nex3 commented May 15, 2023

As another anecdote, I did try parenthesizing the case clause when I tried using it with the ternary operator 🙂.

@munificent
Copy link
Member

(Note that from a user's perspective, if expressions do exist in a collection context, just not anywhere else.)

Technically, those aren't expressions either. They're elements. Because the things in the branches aren't always expressions: ...foo is a valid collection element but not a valid expression. Thus if (c) ... foo is a meaningful collection element but not a meaningful expression.

Part of the reason this works is that collection elements have a natural way to handle zero values being produced, which is what you get from an else-less if whose condition is false. If we supported if expressions, we'd have to either require else or figure out what happens when the condition is false and there's no else. Certainly tractable, but I don't know how much willpower there is to bend the language towards expressions in this way.

@nex3
Copy link
Member Author

nex3 commented May 18, 2023

That's why I said "from a user's perspective" 😆. You and I both know the spec doesn't think about them as expressions, but when you're just writing code what really matters is "I can use if/else` in this place where I can use expressions but not that other place".

Anyway, my vote here is to allow if in any expression position. I'd be happy to see it defined as "if without else returns null if it fails", but I'd also be totally fine with "else is required" or even just "case is allowed in ternary expressions".

@Clavum
Copy link

Clavum commented Dec 8, 2023

To me, a case expression would be most useful in obtaining a boolean for whether an object is a match to a certain pattern.

Where a case expression could give us a one liner:

final isMatch = foo case Bar(baz: Qux());

The only equivalents today seem unnecessarily verbose.

final isMatch = switch (foo) {
  Bar(baz: Qux()) => true,
  _ => false,
}
// bool checkMatch(Object foo) { <- for context
  if (foo case Bar(baz: Qux()) {
    return true;
  } else {
    return false;
  }
// }

I think another benefit of treating cases as a boolean expression would be that it is much more intuitive to use them with the tertiary ?: operator, given that we already understand it to work on a boolean expression.

@lrhn
Copy link
Member

lrhn commented Dec 13, 2023

I too would like if-expressions (and then, maybe, one day, getting rid of ?/: syntax).

But until then, I can work with (foo case Bar(baz: Qux() && var q)) ? q.quxit() : null.

With a grammar of

<expression> ::= 
    <expressionWithoutCase>
  | <caseExpression>

<expressionWithoutCase> ::=
    <assignableExpression> <assignmentOperator> <expression>
  | <conditionalExpression>
  | <cascade>
  | <throwExpression>

<expressionWithoutCascade> ::=
    <expressionWithoutCascadeAndCase>
  | <caseExpressionWithoutCascade>

<expressionWithoutCascadeAndCase> ::=
    <assignableExpression> <assignmentOperator> <expressionWithoutCascade>
  | <conditionalExpression>
  | <throwExpressionWithoutCascade>

<caseExpression> ::= 
   <expressionWithoutCase> `case' <pattern> (`when` <expressionWithoutCase>)?

<caseExpressionWithoutCascade> ::= 
   <expressionWithoutCascadeAndCase> `case' <pattern> (`when` <expressionWithoutCascadeAndCase>)?

By placing it so high in the syntax hiearachy, it'll need parentheses almost everywhere.
The places where it won't are directly in expression lists (argument lists, list literals, record literals), and as the RHS of a declaration.

And no nesting unparenthesized case-expressions inside the matched value expression or when expression of a case expression! 😱

The condition of a ?/: expression will need to be parenthesized.

(But we can treat if-case as just the general case of if (expression) where expression is a <caseExpression>, and not need an extra syntactic construct for it.)

Semantics is that the caseExpression evaluates to a boolean, the bindings are only available on the true branch (code dominated by the expression evaluating to treu), and only until the end of the surrounding statement block. Then:

if (!(foo case Bar(:var bar)) return;
bar.barbarbar();

will work, and any other logical combination.

I think users will understand that, it's exactly the same scope where promotion works.

@lrhn lrhn added the feature-completeness A special or edge case of another feature which isn't supported label Apr 15, 2024
@RepliedSage11
Copy link

I have instinctively tried to transform this:

bool _validateUri(Uri uri) {
      return uri.path == '/path' &&
          uri.queryParameters['param1'] == 'param1' &&
          uri.queryParameters['param2'] == 'param2' &&
          uri.queryParameters['param3'] != null &&
          uri.queryParameters['param4'] != null;
}

into this:

bool _validateUri(Uri uri) {
     return uri.path == '/path' && uri.queryParameters case {
         'param1': 'param1',
         'param2': 'param2',
         'param3': String _,
         'param4': String _,
     };
}

and was surprised that this is not a valid syntax. Are there any plans for this feature?

@lrhn
Copy link
Member

lrhn commented Sep 12, 2024

No concretely worked-on plans for expression-case-if.

I personally (still) think a case-expressions is the optimal generalization, so (expr case pattern when expr) would be an expression, and the bindings are available on the true-branch when the expression is used for control flow branching.
Then (value case var nonNullValue?) ? something(nonNullValue) : somethingDefault would just work, or in your case:

bool _validateUri(Uri uri) {
     return uri.path == '/path' && (uri.queryParameters case {
         'param1': 'param1',
         'param2': 'param2',
         'param3': String _,
         'param4': String _,
     });
}

Parentheses would likely be required when used as sub-expression of another expression.

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 feature-completeness A special or edge case of another feature which isn't supported patterns Issues related to pattern matching.
Projects
None yet
Development

No branches or pull requests

7 participants