dart-lang / language

Design of the Dart language
Other
2.66k stars 205 forks source link

Switch elements - switches in collections, like `if` and `for` #2124

Open lrhn opened 2 years ago

lrhn commented 2 years ago

The patterns proposal includes an expression switch.

It would likely be very useful to allow it as an "element switch" as well, so that it can be used in collection literals with "element"s as values instead of expression.

(Most likely, it requires duplicating the grammar into the elements grammar as well).

That would allow something like:

[42, 37, switch (Endian.host) {
    case Endian.little: ...[87, 0, 0, 0]
    case Endian.big: ...[0, 0, 0, 87]
}, 99]

(just picking a random platform enum as example, bigger enums that can't be handled easily by if would be a better example).

Grammar

<element> ::= <expressionElement>
  | <mapElement>
  | <spreadElement>
  | <ifElement>
  | <forElement>
  | <switchElement>  (* new *)

(* All new: *)
<switchElement>         ::= 'switch' '(' <expression> ')'
                            '{' <switchElementCase>* <switchElementDefault>? '}'
<switchElementCase>     ::= ('case' <guardedPattern> ':')+ <element>
<switchElementDefault>  ::= 'default' ':' <element>?

The grammar is different from switch statements in that:

Semantics

Matching and binding semantics are the same as for switch statements, the scope is the following element.

As usual, elements guarded by multiple cases with "fallthrough" can only use bindings that have the same type in every pattern binding leading to that element.

A switch element is "must exhaust" in the same cases as a switch statement (switching on enum, bool, Null, sealed type, or combinations of those). It's otherwise not "must exhaust" since it's OK to not introduce any elementrs. A "must exhaust" switch can use default: as a default no-element catch-all.

Comments.

I wanted to allow else: instead of default:. It's slightly harder to parse a prior if, though. (I'd even have allowed else element without a :, but that's definitely not possible.)

var x = [switch (e) { 
  case p1: if (b2) e3
  else: b4
}

Not completely ambiguous, but a little confusing. Probably just keep using default:, or case _: with an element.

If adding this, do add null-aware element at the same time, ? expression can be written as switch (expression) { case var v?: v }, but ...? optList can be written as switch (optList) { case var list?: ...list } too, and we still think it's a good operator.

munificent commented 2 years ago

Agreed!

munificent commented 1 year ago

So it turns out the proposal is in an inconsistent state. It doesn't actually propose or specify switch elements. But it does refer to them in a couple of places.

(I believe what happened is that in our discussion around the syntax for switches, I deliberately wanted to consider expressions, statements, and elements holistically. And from that I jumped to assuming I'd already put switch elements in the proposal when I haven't.)

For now, I'm going to remove references to them from the proposal so that it's in a consistent state.

Mike278 commented 2 months ago

Came across this the other day when I wanted to use switch expression to create entries in a map literal, where both the key and value depend on the body of the switch expression - something like:

Map<String, List<String>> _parseChunks(List<List<String>> input) => {
  for (final chunk in input) switch (chunk) {
    ['%', ...final tail] => 'begin': tail,
    ['&', ...final tail] => 'end': tail,
    [...final xs, final last] => xs.join(): [last],
    [] => throw StateError('chunks are never empty'),
  }
};

Is this something that switch elements would support?

mateusfccp commented 2 months ago

Came across this the other day when I wanted to use switch expression to create entries in a map literal, where both the key and value depend on the body of the switch expression - something like:

Map<String, List<String>> _parseChunks(List<List<String>> input) => {
  for (final chunk in input) switch (chunk) {
    ['%', ...final tail] => 'begin': tail,
    ['&', ...final tail] => 'end': tail,
    [...final xs, final last] => xs.join(): [last],
    [] => throw StateError('chunks are never empty'),
  }
};

Is this something that switch elements would support?

Probably, although I think the last case of your switch expression would still fail with Expressions can't be used in a map literal. I don't know if there's a workaround for this, as it also happens with collection-if expressions.

Map<String, List<String>> _parseChunks2(List<List<String>> input) => {
  for (final chunk in input)
    if (chunk case ['%', ...final tail]) 'begin': tail
    else if (chunk case ['&', ...final tail]) 'end': tail
    else if (chunk case [...final xs, final last]) xs.join(): [last]
    else throw StateError('chunks are never empty'), // Error: Expressions can't be used in map literals
};
mateusfccp commented 2 months ago

Just remembered that there is already an issue to cover this last case: #2943.

The current workaround would be to throw in the key and use a "fake throw" in the value so the entry evaluates as Never: Never, which will always be a subtype of whatever map we are constructing.

So, something like this:

Map<String, List<String>> _parseChunks(List<List<String>> input) => {
  for (final chunk in input) switch (chunk) {
    ['%', ...final tail] => 'begin': tail,
    ['&', ...final tail] => 'end': tail,
    [...final xs, final last] => xs.join(): [last],
    [] => throw StateError('chunks are never empty'): throw '',
  }
};
Mike278 commented 2 months ago

the last case of your switch expression would still fail with Expressions can't be used in a map literal.

Ah right, that got lost in translation between my original code and the example. I'm mainly curious about a switch expression evaluating to a map entry.

mateusfccp commented 2 months ago

the last case of your switch expression would still fail with Expressions can't be used in a map literal.

Ah right, that got lost in translation between my original code and the example. I'm mainly curious about a switch expression evaluating to a map entry.

There shouldn't be a problem. As I understand this issue, it's generalized for collection literals, not specifically list literals.