-
Notifications
You must be signed in to change notification settings - Fork 205
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
Allow Multi-Statement Bodies in Switch Expressions #3117
Comments
This is basically a request for "expressions statements" #132, which allows statements inside an expression, which still evaluates to a value. There is no reason to limit that to switch expression case bodies, it's useful in other places too. I think the closest existing issue is #132. I'm going to close this as a duplicate of #132, because I think it's more likely that we'll get the general feature, than a limited feature only for switch expressions. |
Note that this isn't entirely a request for block expressions. In the suggested syntax: switch (someInt) {
0 {
var record = await asyncGetRecord();
record.$1;
},
// Evaluates to void. Gives you the ability to explicitly opt out
// of exhaustivity.
_ {},
} Note how there's no I'm going to re-open this because I could potentially see us allow block bodied switch expression/statement things without going all the way to general block expressions. |
I definitely prefer the syntax here that mirrors functions: If I were Dart czar and could make whatever I wanted happen WRT this constellation of interrelated ideas, this is what feels cleanest to me:
My reasoning is as follows: FWIW, I've started using IIFE's in my switch expressions. They feel a bit hacky/gross and they shadow |
This particular situation has come up a lot for me when converting Sass to Dart 3 style. I'll have a nice lovely switch expression going, and then I'll realize that one or two cases need intermediate variables or an |
I simply do not believe that a "sequence of statements with an expression at the end" as a single level exception inside switch expressions will be enough. It will take approximately eight seconds from us releasing that to someone saying,
And sure, we can tell them to do: switch (e) {
case p {
var result;
stmt;
stmt;
if (condition) {
stmt;
result = e1;
} else {
stmt;
result = e2;
}
result
} But they'll complain that that looks stupid and unnecessary. And they'd be right. Or someone wants to break out early with a result, instead doing the rest of the computation if some value is Statements inside expressions is a very useful feature in many different places, not just here. If we can't even reach feature parity with IIFE's, I'm not ready to call it good enough. |
Others in the eg: switch (e) {
// evaluates to `e1`.
case a {
e0;
if (true) {
e1;
} else if (false) {
e2;
},
// evaluates to `e2`.
case b {
e0;
if (false) {
e1;
} else if (true) {
e2;
},
// evaluates to `e0`.
case b {
e0;
if (false) {
e1;
} else if (false) {
e2;
},
}; That could be applied here for multi-statement switches. That said, I really don't love this as it's very subtle/implicit behavior.
There's an unavoidable tradeoff here. You can't let return short circuit the case without shadowing return to short circuit the function enclosing the switch. So the person requesting this could be rejected on the grounds that the tradeoff isn't worth it (which I believe even outside of my admittedly contrived hypo here). Generally, I've come around to the idea that if Dart is going to have multi-statement bodies in switch expressions, it probably shouldn't do so only in switches as that just creates an inconsistent language. If a switch is multi statement why not an In the meantime, I'm reasonably sold on IIFE's and have been happily using them in my projects. There's nothing stopping anyone from using them today, but they feel very "icky" at first glance. This is drifting fairly far off topic so I'm happy to file a new issue about it if that makes sense, but do we want to/can we make them feel more idiomatic? Are there clear levers to pull to do so? Like linter changes that would encourage them in some contexts? Using them where applicable in prominent Dart repos (ie Flutter)? Including an example in the Dart docs for writing a multi-statement switch expression using an IIFE and recommending it over converting to a statement with return (possibly nested in a helper function)? My one big gripe with IIFE's in this scenario is that the syntax is pretty subtle. You can't tell that a function expression is immediately invoked until you've finished reading it's body. // Switch expression used as a statement.
return switch (e) {
case a: print('foo'),
case b: () {
print('foobar');
print('baz');
}, // <--- forgot to invoke, prints are never executed
} I guess you could say this is my fault for using a switch expression as a statement when a switch statement would work, but to that my answer is: #3061 |
Here's a pitch (it's not amazing, but it's not completely stupid either): A <expression> ::= ...
| <label>? 'do' <statementExpressionBlock>
<statementExpressionBlock> ::= '{' <statement>* <expression>? '}' The That's combined with a "break-with-value", and making all control flow operators expressions: <controlFlowExpression> ::=
'return' <expression>?
| 'continue' <identifier>?
| 'break' <identifier>? ('(' <expression> ')')?
| 'throw' <expression> -- was already an expression, but still is.
| 'rethrow' If the break targets a Then you can use The
Similarly a That allows you to write: var lastIfAny = do { var tmp = this._tmp; if (tmp == null) break(null); while (tmp.next != null) tmp = tmp.next; tmp }; And then we can also make switch expression case bodies be: <switchExpressionCaseBody> ::= '=>' <expression> | <statementExpressionBlock> and let the default target of the break-with-label inside that Alternative syntax for var lastIfAny = do { var tmp = this._tmp; if (tmp == null) ^null; while (tmp.next != null) tmp = tmp.next; ^tmp; };
var lastIfAny = switch (this._tmp) {
null => null
var cursor {
while (cursor.next != null) cursor = cursor.next;
^cursor
}
}; At that point (And you still have to write |
If we rewrite the original var lastIfAny = do {
var tmp = this._tmp;
if (tmp == null) break(null);
while (tmp.next != null)
tmp = tmp.next;
tmp
};
var lastIfAny = switch (this._tmp) {
null => null,
var cursor {
while (cursor.next != null)
cursor = cursor.next;
cursor
}
}; In my opinion, it's quite readable and a neat idea. I understand that it's common for an expression-based language to evaluate the last expression as an exit/return. However, in Dart, I believe that using a reserved keyword ( I have a suggestion for discussion (which might be considered as a somewhat foolish idea): How about treating labels as names that can only be defined once? This change, although subtle, would be breaking. outerLoop:
for (int i = 1; i <= 3; i++) {
final outerLoop = 0; // The name 'outerLoop' is already defined.
print('Iteration $i');
if (i == 2) break outerLoop;
} With this, I think we can replace var lastIfAny = do {
var tmp = this._tmp;
if (tmp == null) break null;
while (tmp.next != null)
tmp = tmp.next;
break tmp;
};
var lastIfAny = switch (this._tmp) {
null => null,
var cursor {
while (cursor.next != null)
cursor = cursor.next;
break cursor;
}
}; Independently of the syntax I really like @lrhn overall approach because we don't have to write |
I run into this all the time too.
I did add support to the formatter to collapse switch cases to a single line if they could all be collapsed. I implemented that exactly because of this concern: I didn't want people to feel compelled to use switch expressions just because they're vertically shorter. But it was a high-churn change with some performance issues. We had enough going on getting Dart 3.0 out the door that I backed out the change. I would like to revisit it again at some point. |
(Can I haz |
Yes, please. I've considered that before. It's definitely (potentially) breaking, but might be worth it. If the labels exist in the same scope as variables, then there is no ambiguity in |
For what it's worth, my impression is that, whether or not we pack single statement blocks onto one line, many users just aesthetically like the way |
If we disallow labels in switches, maybe we could just allow you to omit Might require some lookahead to see if something is followed by
Nope, the ?/: conditional expression gets in the way again. (Or say that an expression statement cannot start with a conditional expression or function literal. |
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?
switch
, I have to think carefully about which syntax meets the current scenario: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).
print
statement for debugging.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:
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 ofreturn
. Yield is a bit confusing as it's pretty associated with generators. Also this will shadow the function levelyield
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:
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:
I don't know if I love the following as it's awkward/verbose, but allowing
{}
for the empty case squares this circle:Cons
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
).The text was updated successfully, but these errors were encountered: