Skip to content

Allow Multi-Statement Bodies in Switch Expressions #3117

Open
@caseycrogers

Description

@caseycrogers

This is a spinoff issue from the following:
#3061

I propose we create a new syntax to allow multi-statement bodies inside of switch expressions.

Why?

  1. Having a one statement expression often gets very hard to read. Especially when doing complicated and/or async processing:
return switch (someInt) {
  0 => 'No message',
  // Could use `then`, also poor readability.
  // Gets much worse as more layers of processing are needed.
  int i => (await asyncGetRecord(i)).$2,
};
  1. Having independent syntaxes for switch statements and switch expressions is inconvenient, bringing the switch expression into closer feature parity with the switch statement would allow it to be used in many places where only a switch statement is usable/practical today. Right now, every time I have to write a switch, I have to think carefully about which syntax meets the current scenario:
  • Do I want it to evaluate? Then expression syntax.
  • Oh wait, I need multi-statement bodies, better use statement syntax with return inside a helper function
    ^this is a really annoying refactor
    These tradeoffs are pretty non-obvious and especially confusing given that both types of switches use the same keyword which implies feature parity. If the expression syntax more or less has feature parity, then it becomes the safe default (and probably rapidly just becomes the approach developers use in all scenarios in practice).
// This is a switch statement that could be easily converted to a switch
// expression if multi statement bodies were supported.
switch (someInt) {
  case 0:
    someSideEffect();
    someOtherSideEffect();
 default:
    defaultSideEffect();
}
  1. Allowing a switch expression that also side effects. This one is pretty weak as many will (reasonably) consider this an anti pattern, but I figured I'd include it for completeness. In the simplest case, this could just be a print statement for debugging.
  2. This one is pretty controversial, but one I believe strongly in it and I think it's worth mentioning. Though I believe this proposal is valuable whether or not you believe in this point. With multi-statement expression bodies, we've largely eliminated the need for the statement syntax and it could be a lint against it for those with strong opinions or, if the community really takes to using the expression everywhere, we could consider deprecating the statement syntax to reduce language complexity.

Syntax

There are a lot of different ways to do this-I don't have the strongest opinions. Here are some options I've come up with and have seen suggested by others:

  1. Curly braces that evaluate to the last line in the block-as in expression based languages.
switch (someInt) {
  0 {
    var record = await asyncGetRecord();
    record.$1;
  },
  _ => 'some string',
}

This has minimal verbosity and maps pretty well to single statement vs multi-statement anonymous functions. However, it's pretty implicit and may take a fair amount of adjustment for developers to get use to it.
1a. Same, but instead of implicitly evaluating to the last line, you use return. This has the most parity with functions which will make it more intuitive, but it shadows the function level return so you can't short circuit out of the switch-not great.
1b. Same, but yield instead of return. Yield is a bit confusing as it's pretty associated with generators. Also this will shadow the function level yield if you are using a switch in a generator.
2a-b. Same as 1a-b, but arrow and then curly braces _ => { ... }
I don't love that this conflicts so heavily with anonymous function syntax-every time the developer goes to write a multi statement switch body they're going to forget to write the arrow because they have muscle memory from writing functions. But it does make it very clear that the following block evaluates.
3. TBD? I can edit this original post to add more proposals if they come in.

All of the above have downsides, but I guess I prefer 1?

One advantage of all the above options is that you can use empty curly braces to signify the empty case. Using 1 as an example:

switch (someInt) {
  0 {
    var record = await asyncGetRecord();
    record.$1;
  },
  // Evaluates to void. Gives you the ability to explicitly opt out
  // of exhaustivity.
  _ {},
}

There's some debate already on how to handle switch elements (eg a switch statement inside a list literal). These do not need to be exhaustive but we now have a conundrum:

  1. Switch statements need not be exhaustive, but they don't evaluate to anything so we can't use them
  2. Switch expressions evaluate, but need to be exhaustive so we could only use them if we had special case non-exhaustivity
    I don't know if I love the following as it's awkward/verbose, but allowing {} for the empty case squares this circle:
var myList = [
  // This is exhaustive, but only cases that evaluate to a non-void
  // value are included in the list.
  switch (someInt) {
    // Not included.
    0 {},
    // Same.
    1 => print('foo'),
    // Included. Note could also be an iterable preceeded by the spread operator.
    var i => MyObject(i),
  }
];

Cons

  1. Things that evaluate should not have side effects. The restrictive nature of the expression syntax (namely single statement bodies) makes it much harder to shoot yourself in the foot with side effects.

While this is a reasonable and common philosophy, I think it'd be uncharacteristically opinionated for Dart to enforce it here, especially given that even pure switch bodies without side effects stand to benefit from multi statement bodies (see Why #1).

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problemsstate-duplicateThis issue or pull request already exists

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions