Open your-diary opened 1 year ago
But according to this section of the specification:
Thanks to expression statements, a switch expression could appear in the same position as a switch statement. This isn't technically ambiguous, but requires unbounded lookahead to read past the value expression to the first
case
in order to tell if a switch in statement position is a statement or expression.To avoid that, we disallow a switch expression from appearing at the beginning of an expression statement.
@MaryaBelanger
For the future readers:
According to the spec
In the rare case where a user really wants one there, they can parenthesize it.
So this code compiles:
enum Color { red, blue, green }
void main() {
final color = Color.red;
(switch (color) {
Color.red => "red",
Color.blue => "blue",
Color.green => "green",
});
}
I'll add all this to the site docs, thank you both
Unfortunate that a different keyword, like match, wasn't utilized for the expression form. This parenthesized form is not ideal.
@gamebox
IMHO, using the same keyword both for the statement and the expression forms is a good idea to make the language simple. Actually, Rust doesn't have switch
and just uses match
keyword for both of the forms.
The essential problem here is, as written in the spec,
Thanks to expression statements, a switch expression could appear in the same position as a switch statement. This isn't technically ambiguous, but requires unbounded lookahead to read past the value expression to the first case in order to tell if a switch in statement position is a statement or expression. To avoid that, we disallow a switch expression from appearing at the beginning of an expression statement.
This implies improving the parser would fix this issue without redesigning the language itself (e.g. introducing a new keyword for the expression form). This may also mean improving the parser in such a way would make compilation speed slower.
Anyway, I hope this limitation will be removed in the future.
Yes, that's why I'm saying that having different keywords for the statement versus the expression (which has different syntax anyway), this ambiguity goes away
A different onset like switch on (<expr>)
may have been a good disambiguator as well. But the syntax is now the syntax, so this is crying over spilt milk 😉
Can we re-open this? I get that this is in spec, but lets repurpose this issue as a proposal to change the spec? Or I can file a new issue if that's preferrable.
The way I assumed the new switch syntax was going to work was that this is simply the new syntax, and while the old syntax may stick around for backwards compatibility, it is overly verbose/awkward and will rapidly be considered an anti-pattern.
But instead I now either have to live with two inconsistent syntaxes in my code base or I have to do the weird paren hack. Both of these seem pretty undesirable, especially given how undiscoverable the paren hack is.
Also, this line from the spec just seems incorrect to me:
In the rare case where a user really wants one there, they can parenthesize it.
This is not a rare case. Anywhere where the old syntax is used I (and I think most devs) want to use the new syntax instead for consistency and because it's just flat out more writeable and readable. This is tricky because the new syntax doesn't have full feature parity with the old syntax. The three examples I can think of:
final someString = switch (someInt) {
// All the lines execute and the case evaluates to the last expression.
1 {
print(1);
someSideEffect();
"foo";
},
2 => "bar",
_ => "baz",
}
This just ports syntax conventions even further from functions where =>
is just syntactic sugar for a single-line function body. However it could be confusing in that devs may assume that they need a return and not realize that that'll return the function, not the case block.
I don't think it's just me that finds the bifurcated syntax confusing, see engagement on a related tweet: https://twitter.com/caseycrogers/status/1658946335004037120?t=L9meQwQaojmW6I2DFkSE5Q&s=19
I know very little about compilation and parsing so I don't know the best way to implement a fix here-I'm just describing what I think the ideal customer-facing behavior is.
The way I assumed the new switch syntax was going to work was that this is simply the new syntax, and while the old syntax may stick around for backwards compatibility, it is overly verbose/awkward and will rapidly be considered an anti-pattern.
Switch statements are not old syntax that is going away. They are the idiomatic way to do multi-way branching in a statement position. A switch statement is what you need if any case body needs to be a statement and not an expression. Also, switch statements can contain multiple statements in a case body, and they can have multiple cases share a body.
Switch statements are great. If you are writing a statement, and you want it to switch, a switch statement is the idiomatic way to do it. That was true before Dart 3.0 and is true today. We didn't deprecate switch statements. We made switch statements much more powerful by allowing them to contain patterns in their cases.
We also added switch expressions, because you can't use a statement where an expression is needed.
Just as most Dart users wouldn't write:
main() {
someCondition ?
print('True');
:
print('False');
}
And would instead use the statement form:
main() {
if (someCondition) {
print('True');
} else {
print('False');
}
}
Instead of:
void main() {
final color = Color.red;
(switch (color) {
Color.red => print("red"),
Color.blue => print("blue"),
Color.green => print("green"),
});
}
The idiomatic thing to write today is the same as it was before Dart 3:
void main() {
final color = Color.red;
switch (color) {
case Color.red: print("red");
case Color.blue: print("blue");
case Color.green: print("green");
}
}
Note that the original example is a good example for why a switch expression isn't what you want:
void main() {
final color = Color.red;
(switch (color) {
Color.red => "red",
Color.blue => "blue",
Color.green => "green",
});
}
This code compiles, but it doesn't do anything. You have a switch expression that does nothing but produce a value which is immediately discarded because the switch expression is in a statement position.
If this had been written as a switch statement, it becomes more obvious that those case bodies are pointless.
FYI we did try to adjust the site docs based on this issue. Albeit as brief and "language tour"-like as possible:
Switch statements are great. If you are writing a statement, and you want it to switch, a switch statement is the idiomatic way to do it. That was true before Dart 3.0 and is true today.
I get that this is the intent of the Dart team-I'm proposing that, at the very least, Dart shouldn't arbitrarily limit where developers can use the expression syntax (I'll quit saying "old" and "new" because that was unnecessarily charged language on my part).
Here is my reasoning for allowing developers to use the expression syntax at the start of statements (please forgive me if I use some of the wrong terms here-I'm approaching this as a developer using the language, not as a language designer):
case
, break
, continue
, default
, labels where an arbitrary token behaves kind of like a keyword). I think re-using the existing relevant Dart symbols/conventions as much as possible results in a lot less cognitive load for a developer trying to write a switch. A switch expression with pattern matching does this by re-using ( ... ) => ...
from single statement anonymous functions and _
from wildcard variables.:
to signify a multi statement code block instead of {}
To respond to your points directly:
Just as most Dart users wouldn't write: [ternary as statement] And would instead use the statement form: [if..else] The idiomatic thing to write today is the same as it was before Dart 3:
I'd posit that ternaries as statements are non-idiomatic because ternaries have poor readability and are too opinionated (require exhaustivity, don't support multi-statement bodies, don't support more than 2 cases, confusingly flip the <keyword> <clause>
ordering used everywhere else in Dart...) when compared to if...else
. Not because developers believe that statements and expressions should use different syntax. There already exists if (<predicate>) <expression>
for single statement if bodies and guards in collection literals-I imagine something similar would've been used for ternaries if it weren't for the exhaustivity requirement?
If Dart wants there to be a sharp switch expression/switch statement demarcation like there is with ternaries and if..else
then maybe the two kinds of switches should've used a different keyword/symbol like ternaries and if..else
?
Note that the original example is a good example for why a switch expression isn't what you want: [switch expression used as unnecessary statement] This code compiles, but it doesn't do anything. You have a switch expression that does nothing but produce a value which is immediately discarded because the switch expression is in a statement position.
Is this really a value add that needs to be forced by a special-case limitation (switch expression can't be used at the start of a statement expression) to the language? AFAIK Dart isn't generally opinionated against using expressions as statements, and I don't see any first-principles specific to switches that suggest it should be opinionated about it here.
Besides, we already have the unnecessary_statements
lint, I'm not familiar with how its implemented, but maybe it could be extended to catch the same problem in the context of switch expressions used as statements?
Also, switch statements can contain multiple statements in a case body...
I wish switch expressions could have multi statement bodies. Maybe my Scala experience is showing too much here, but its philosophy of "every code block is an expression" is REALLY nice to work with and something that I think oughta be ported to other languages wherever it makes sense. As in my previous post, I think switch expressions should allow multi statement cases using { ... }
. This also has the added benefit of letting devs use {}
to signify an empty case body like they already use {}
to signify an empty function body. I'll file a new issue for this (or jump on one if it already exists).
...and they can have multiple cases share a body.
Pattern matching trivially supports multiple cases with the same body. In fact, I find it much more intuitive and readable than the fall-through based approach that switch statements use because it just re-uses the ||
operator that I'm already familiar with instead of an implicit characteristic of switch statements:
var foo = switch (someInt) {
(1 || 2) => 'one or two',
(_) => 'some other number',
};
Expression statements don't have an equivalent to labels, but that seems like a pretty minor loss. Labels are such a weird special-case feature that (IMO) would be just as well if not better served by a helper function/method.
TLDR: I think there are a lot of compelling reasons to allow a switch expression to be used as a statement. If Dart were being remade from scratch today, I think it'd be hard to argue that there should be two distinct switch syntaxes, one for expressions and one for statements. As such, while it may be unreasonable to deprecate the statement syntax (I'd challenge even this claim, though that's probably best left for another time), at the very least lets not place artificial limits on when and where the expression syntax can be used.
Can we re-open this bug or should I file a new issue and port the conversation here?
I've reopened this to discuss on the team.
@caseycrogers, I find all of your points pretty compelling.
I think the existing switch statement syntax is familiar and easier to learn for users coming from some other C-derived language, but I agree that if you aren't steeped in that tradition, it sticks out like a sore thumb. (I could insert an essay here about how the design of switch statements in C is consistent with the rest of the language when you consider a point in time when labels and goto
were idiomatic, but I'll skip that. :) )
I wish switch expressions could have multi statement bodies. Maybe my Scala experience is showing too much here, but its philosophy of "every code block is an expression" is REALLY nice to work with and something that I think oughta be ported to other languages wherever it makes sense.
I have a pretty strong preference for everything-is-an-expression languages too. But the initial designers of Dart did not and it's not clear to me if there is a path to get there that doesn't leave a lot of confusion and mess in its wake. I'm interested in exploring this, but I suspect it will be a bridge too far.
As in my previous post, I think switch expressions should allow multi statement cases using
{ ... }
. This also has the added benefit of letting devs use{}
to signify an empty case body like they already use{}
to signify an empty function body. I'll file a new issue for this (or jump on one if it already exists).
Filing a separate issue (if you haven't already, I'm a little behind on GitHub) would be great.
Filing a separate issue (if you haven't already, I'm a little behind on GitHub) would be great.
They did (#3117), and I closed it, deferring to #132, which is about allowing statements in expressions in general, not just in switches. Because that'd be generally useful.
We can also discuss allowing a terser syntax for switch statements here, which would be the other direction to go.
(Just allowing you to omit the case
is not going to be great. I also don't want to comma-separate statements.)
@munificent thanks for reading through my whole post on the matter, I hope you all end up allowing switch expressions at the start of statements!
For multi-statement case bodies, I'll go join the conversation on the relevant conversation now :)
According to the official documentation,
However, the code below doesn't compile (DartPad):
The error message is
On the other hand, these code compile:
Why?
Note, in Rust, the equivalent code compiles: