language icon indicating copy to clipboard operation
language copied to clipboard

If-case expressions

Open nex3 opened this issue 2 years ago • 11 comments

(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 avatar May 11 '23 03:05 nex3

~~Feels like this issue https://github.com/dart-lang/language/issues/2537 has a discussion about this problem.~~

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

incendial avatar May 11 '23 08:05 incendial

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.

munificent avatar May 11 '23 23:05 munificent

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.)

nex3 avatar May 12 '23 23:05 nex3

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.

lrhn avatar May 12 '23 23:05 lrhn

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

nex3 avatar May 15 '23 19:05 nex3

(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.

munificent avatar May 18 '23 21:05 munificent

That's why I said "from a user's perspective" :laughing:. 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".

nex3 avatar May 18 '23 22:05 nex3

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.

Clavum avatar Dec 08 '23 14:12 Clavum

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! :scream:

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 avatar Dec 13 '23 16:12 lrhn

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?

alex-medinsh avatar Sep 12 '24 18:09 alex-medinsh

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

I personally (still) think a case-expressions is the optimal generalization, so <Code>(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.

lrhn avatar Sep 12 '24 19:09 lrhn