-
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
Switch expressions with some cases that need a body #3065
Comments
The fact that were using => makes me really wish that we could write: final a = switch (obj) {
pattern {
return 42;
}
} (with or without a ":" after the pattern) I often switch back and forth between => and {return} depending on what's most readable for the given situation. |
That would be most consistent with function body / function expression bodies, which is actually probably more consistent for the feature than using switch statement body syntax. I don't know which would fit into the grammar better. |
We have had some proposals about a construct which is often called 'block expressions'. Cf. #2848 (comment). They could do the job, and they amount to the same syntax except for the parentheses: return switch (this) {
Ref() => k(this),
// ... etc
Lambda(:final formals, :final body) => {
//... Some code
return x;
},
}; The block expression is quite similar to the function literal application |
Yes, I do think that this featured could be more general as in your linked issue. However, since that feature will not be available for awhile most likely, I think it would be good to support for a refactoring assist to convert an expression to an immediately invoked function expression. |
I agree this is an annoying wart of the syntax. If I had a time machine, Dart would have always been an expression-based language. But I don't, and it isn't, so I tried to fit patterns and switch expressions in as gracefully as I could given that Dart already does make a distinction between expressions and statements. Something like block-expressions could help, though it makes me wonder if we should consider just trying to actually do the whole thing and make the language expression-oriented. |
If you look at the changes recently, they are heavily expression oriented: Collection elements The first two work in a statement oriented language, because collection elements are only useful in expression position, records are part of patterns to a large degree, but as just a datastructure they are mostly a anonymous constructor expression. Patterns as you mentioned is really hard to put into a statement oriented language. I think it is great that it is finally here. I wouldn't go back to pre dart 3.0, but the experience has yet to feel polished in my opinion. This can and I believe will come over time, however I wonder if there will need to be some larger breaking changes as far as syntax goes to make it actually feel polished.
I'm curious what you envision for this? Is a larger breaking change with an automated syntax migration needed in your opinion to make dart an expression first language? |
My two cents on this is that it looks a bit weird. => {} is reminiscent of JS. It's not something we see in Dart. It'd be quite confusing imo |
Like any other Dart function syntax is expressed this can folllow the same. the user should be able to write either of below |
Yes, I think that would work, though we have to be careful that it doesn't run into an ambiguity around map patterns or get in the way of other pattern syntax we might want to add. The harder question is what the semantics of that block body are. I don't think I would be tempted to do something Rust-like and say that a block yields the value of the last statement in it (which would be But making the language expression oriented like this would be a big change, and I have no idea if there is a coherent design that would hold together and, if so, if we could actually ship it and migrate the world onto it without too much chaos. |
I humbly propose the do-give expression:
The In the switch example:
|
To reuse syntax, we could use |
I guess the first question is, can you If |
I would say we shouldn't allow
I can see how these restrictions may cause other problems. For instance, if one want to build a list inside the Between |
|
If we add a way to have statements inside expressions, we can indeed choose to either allow or disallow local control flow statements like I'd personally allow them. It can make some things harder to reason about, but writing opaque code isn't hard today, you're just expected not to do it. var value = expression;
if (value == null) return null;
... foo(value) ... If we disallow control flow operators in statement expressions, we can consider also disallowing them in finally blocks. That's another place where doing explicit control flow can get hard to reason about. We should not allow statements only in switch expressions. Then people will write If we can agree on behavior, I think it will be possible to find a syntax. We can easily allow More expressions could be allowed using a block, But I also want to be able to nest such statement expressions, and not necessarily exit the innermost one. … (label: foo(do{stmt;if (b) break label: 42;…}) Again, of we can agree to have the functionality, we can start to look for a good syntax. |
This syntax is so verbose it defeats the very purpose of the exercise IMO, which is: to allow writing short fragments of code that otherwise don't fit into a single expression (under current definition of expression). "Short" is a key here: for longer programs, the current syntax with function literals The above example … (label: foo(do{stmt;if (b) break label: 42;…}) can now be written as |
I think that allowing small and restricted statements, but not the full power of statements, is always going to be just short of powerful enough for what people want. Might as well make it full power. If you just need to do one computation before another, you can use; T seq<T>(void _, T value) => value; and write If it's to introduce a local variable, like If it's to allow local control flow using So, if we want to allow statements inside expressions, we should allow all statements. All we need to do is agree on semantics, and then syntax. :) For syntax, the simplest is the Being short is nice. We could allow var norm = (
var r = 0;
for (var i in xs) {
r += i * i;
}
sqrt(r));` Using Lots of options. If we can agree on the semantics, we should be able to find a useful syntax to match it. |
In this case, I think I disagree on the semantics proposed in this issue in favor of something like #1052. If I understood correctly, with this we could do (adapting from original example): return switch (this) {
Ref() => k(this),
// ... etc
Lambda(:final formals, :final body) =>
let a = _buildAFromFormals(formals) in
let b = _doSomethingWithAToGetB(a) in
b.processBody(body);
}; |
A radical solution would be: just saying that the "block statement" (as defined in 18.1 of the spec) implicitly returns a value (thus effectively becoming a "block expression") and this "returned value" is determined exactly as in other languages supporting block expressions (there's a lot of prior art). (Essentially, it always returns the "last computed value"). var five= {
fn_call();
5;
} What is the downside? I can't think of any. Right now, the construct {
fn_call();
5;
} is already legal in dart anyway, it's just very rarely used by anyone except a couple of people from dart team. If this is OK, then the example from the original post can be written as return switch (this) {
Ref() => k(this),
// ... etc
Lambda(:final formals, :final body) => {
//... Some code
x; // assuming x is defined by "some code"
},
}; Similarly, there's no harm in saying that "if-else if ... else" implicitly returns the last computed value (some restrictions apply - e.g. the case with no "else" won't implicitly return anything): var x= if (a<b) 3; else 4; // works
var y= if (a <b) 3; // error
var z= if (a<b) { // works, rules for block expression apply here
stmt;
5;
} else {
stmt;
3;
} There are only two constructs that support "implicitly returned value": block expression and if-expression (plus already defined switch-expression). There's no "while"-expression or "for"-expression or anything else. There's a small catch though: "early return" from block expression is problematic. "break value" is no good (conflicts with |
If we say that a The type of a statement with type context C is:
That can work. I am worried about parsing it, for all the same reasons we don't allow an expressions-statement to start with a var x = {if (test) {} else {}}; Is that a statement block or a set-of-maps literal? (The |
Parsing can always be solved by adding an extra symbol on the opening bracket. This would help with readability too I think. var x = #{
return 42;
}; And if we opt for this, I'd heavily suggest making the Rather than: final a = switch (v) {
_ => #{ return 42; }
} We'd have one of: final a = switch (v) {
// Desugared to `=> #{...}`?
_ #{ return 42; }
}
final a = switch (v) {
// Reminiscent of switch statement, for syntax parity.
// Then I'd suggest supporting `=>` in switch statements to be complete.
case _: #{ return 42; }
} |
I think it's a contrived example. If someone wanted just an if-expression, the code would be different: As for "do" - for some reason, I don't like it, it just feels wrong. Especially considering that we already have "do - while" in the language, which doesn't return a value and never will. Maybe @rrousselGit : there should be no "return" in your examples. Block "returns a value" not via "return", but implicitly. final a = switch (v) {
case _: #{ stmt; return 42; } // returns from the containing function!
} would mean a totally different thing than final a = switch (v) {
case _: #{ stmt; 42; }
} That's why I suggested disallowing "return" in block expressions. @lrhn: this is problematic
This clashes with the existing treatment of if-else in collection literals. Not sure what can be done about it. |
Definitely a contrived example. I expect almost all possible reasonable programs to be uniquely parsable, and the test will be contrived because it's obvious to a human reader what the author means. Then there is the issue of readability. If the meaning of a long piece of code changes significantly whether there is s semicolon or not, then it breaks with the design principle of "make similar operations have similar syntax, and different operations look different". The first part is to make learning the language easier. The second is to make code readable. And statements in expression position will definitely clash with elements. We have three different kinds of code:
(If we had conditional arguments, we'd cover the zero-or-over case too.) Those are significantly different, and we can't easily use one instead of the other. At least we always need to know which one it is. |
Not necessarily. That's up to us to decide what the "return" in a custom block does. Especially when using a special syntax for defining the block.
|
Indeed, var x = if (cond) {
stmt;
42;
} else {
stmt;
43;
} Writing "return 42;" is not an option (clearly, it would be a return from the enclosing function). So, we have to introduce "implicit return" for if-expression. But as soon as we introduce implicit returns for if-expression, why do it ONLY for if-expression and not for block-expression? Another example: var x = #{
stmt;
if (test) {
42;
} else {
43;
}
} This won't work: you need to write "return if (test)..." or "return 42"/"return 43". I find it inconsistent and also inconvenient. We may, of course, live without if-expression - I just hoped we can kill two birds with one stone. But without the second bird, the case for @lrhn: does syntax #{...} address your concerns? There's no conflict with the set literal. |
The point is that we'd use var x = if (cond) #{
stmt;
return 42;
} else #{
stmt;
return 43;
} There's no issue here Or: var x = #{
stmt;
if (test) {
// We quit the enclosing #{ block here
return 42;
} else {
return 43;
}
} And of course, we could use this in collections: final x = [
if (foo) #{
statement;
return 42;
}
] |
If we want to call a thing "an expression", then the term itself imposes some restrictions. In particular, you can't use the word "return" in the context of the expression in the same meaning as you do inside a function. Expressions like this are supported in many languages, but there, if you say "return x" inside a block, it will return from the enclosing function (not from the block). See, for example, this thread |
Block/statement expressions "need" a delimiter mainly because statements can already contain expressions. If expressions can just contain statements, then we risk cycles in the grammar. A unique delimiter can make it clear and unambiguous which way we are parsing the following syntax, both to the compiler and to the human reader. The alternative, which we have used so far, is that if something starts a statement, then it's the statement form, otherwise it's the expression form. That has worked fairly well. Then we added elements, which can also contain expressions, and again the rule is that if it can start an element, it does. Expressions are the lowest "precedence" of interpreting ambiguities. If an expression can now start with a statement, things may get weird, because now an element can start with a statement, which is new and grammatically confusing. Delimiters makes the grammar unambiguous and gives the reader a hint about what's to come. "If expressions", which I guess means conditional expressions like (You can totally use the word |
By "If expressions" I meant your proposal #2306. (I don't understand the part where you said this expression cannot start the statement). var x = if (cond) 42 else return 15; Right now, you can't use "return" in expressions. E.g. |
There are lots of ideas floating around here. With that, I would allow (whether int foo(bool cond) {
var x = cond ? 21 : return 15; // aka: `int x; if (cond) x = 21; else return 15;`
return x + x;
} That would be the point of allowing those as expressions. |
If "return" is allowed in expressions, then what stops dart from simply reclassifying a block, "if" and "try" from statements to expressions, with no syntax changes to the language? We can say that these constructs always produce a result; whether the result is used (e.g. assigned to a var or passed as a parameter, etc.) is irrelevant. An "early return" from an expression has the form (there's a small backward compatibility issue with the "break" in a legacy "block statement", but I think it's solvable) |
Whatever syntax is decided, I think it is critical to allow returning to the containing function from any nested expression. |
One of the problems with block expressions is that we will have a third way to write function definition: (x)=>expr;
(x){...body...}
(x)=>{...block-expr...} To add to the confusion, the way how function returns the result would be different between variant 2 and variant 3. |
I'm not sure having a suboptimal way to write something is a problem, if there is a better way too, and we can point people to the better way. You can do It's not a problem. As for doing int foo() => do {
stmt;
if (something) return 0;
42
}; I think we can recommend converting that to a block body too, as a matter of style. |
Is there a difference between var x= do {
...
if (cond) 42; // semicolon
} and var x= do {
...
if (cond) {
42
}
} In other words, does the semicolon suppress the logic of implicit return? |
There is no specification for expression blocks yet. Nobody knows what the syntax means, we're just throwing around examples. |
I don't quite understand the role of "do" in var x= if (cond) {
Stmt;
42
} else {
Stmt;
43
} There are two block-expressions here: one is then-expression, another is else-expression. Any justification you have for a "do" also applies here. Do we have to add a "do" in each branch? Like this? var x= if (cond) do {
Stmt;
42
} else do {
Stmt;
43
} |
Maybe people dislike IIFE because it's ugly? Then it's possible to introduce a shortcut e.g. |
I dislike IIFEs because they're the wrong tool for the job. They try to look like code that's in the current function, but they are not. It's just a helper function with no name. One could instead make it a named function and call that. If that's not good enough, that's because a function isn't really what the problem calls for. It usually isn't, otherwise that completely idiomatic approach would probably have been used. So it's syntax that tries to be something other than what it is. That makes it hard to read. And it doesn't work well with async code. |
The problem is that in dart, it's difficult to introduce block-expressions without causing great confusion about functions. fn plus_one(x: i32) -> i32 {
x + 1
} means something different than fn plus_one(x: i32) -> i32 {
x + 1;
} The Documentation explains that this semicolon turns the expression into a statement , which I find a bit strange, and don't even understand what exactly is turned into a statement. Because dart is unlikely to drop "return" in functions, the logical thing to do in blocks is to require explicit "emit". Agree? |
What part is the confusion? The example you gave is a top level expression of a function so x + 1 Or return x + 1; Yield the same result. But, if you nested these operations inside another expression, the former would yield to that expression and the latter the function. What is being turned into a satement by adding a semicolon seems self explanatory to me. |
The confusion comes from the fact that today in dart, functions don't support implicit return (not counting an implicit return of null in the function that failed to return anything by the moment it reaches the end of the body). |
I agree, it makes sense for functions to also support implicit returns. But there may be some language constraint I am not aware of that prevents this from being implemented. If so, I don't think it saying implicit returns are only allowed in a |
@lrhn if String func(){
int x = 0;
String y = while(true) {
x++;
if(x < 5) continue;
"string"
}
return y;
} is this what you had in mind? |
We can allow Probably not |
Counterexample: var x = (){
if (false) {
return 42;
}
}(); This is an expression without the value, it returns null. :-) |
Actually, this is an expression with value |
See section 17.11 of language spec v. 3 |
Personally, I dislike having the last statement/expression in a block sometimes be used for its value, without any keyword like In Rust and Scala, blocks are expressions, and in the block expression The JavaScript do-expression proposal uses the syntax Again, though, I feel like it potentially leaves the reader guessing whether what looks like a statement is actually being used for its "result" value, especially if the block is long. A counterargument would be, the Do expressions are unrelated to do-while loops, and the syntactic ambiguity is easily addressed in the TC39 proposal:
|
Suppose I have the following program: var x=(){
...
if (cond) return "Blue";
return "Moon";
}(); Having heard of a do-expression with the "implicit return", I refactor it into a do-expression by simply dropping both "returns" var x= do {
...
if (cond) "Blue";
"Moon";
}; After a bit of testing, I discover that the program now returns "Moon" regardless of the condition, and I don't understand why. In desperation, I put "returns" back var x= do {
...
if (cond) return "Blue";
return "Moon";
}; Hilarity ensues... :-) The idea that I like the word "emit". With explicit var x= do {
...
if (cond) emit "Blue";
emit "Moon";
}; (Using more terse "^" is also an option (there's a precedent in smalltalk, where it meant "return"), but the word "emit" is more meaningful IMO). Explicit "emit" would rule out making "if" an expression, otherwise we have a puzzler here: var x = do {
var y = if (cond) {
emit 42; // emit to where??? y or x?
} else {
emit 0;
}
...
} An argument can be made that |
Among the disadvantages of IIFE mentioned earlier, there's this: IIFE doesn't work well with async code.
What will happen if we replace IIFE with a do-block? Then the plain var x = await do {
if (cond) {
emit Future.value(1);
} else {
emit 1;
}
}; What is the return type of this expression? It (I'm guessing) must be the same as the return type of the expression |
do-blocks aren't meant to replace IIFE. They each have their own use cases with some overlap. I don't think we need async do-blocks. You can just use a do-block inside an async function. But if they are supported, I don't see a problem here. The latter branch can just be coerced into a future, like a regular async function. |
I agree with using a new keyword that breaks out of the new I think the point of bringing up async was just to say to definitely not implement or specify To me, the word "emit" doesn't convey that execution of the If not Whatever is chosen should look good in small blocks like |
Sounds like everyone has a different option on the syntax. I personally like omitting any unnecessary keywords and just being implicit. Coming from a background familiar with Rust, I have never found personally or heard anyone else voice distaste over being implicit in these situations. I think after you code that way for awhile, it actually becomes tedious to specify unnecessary keywords that are just syntactic sugar (Switching back and forth to Dart and Rust, I have even learned to dislike having to use braces in expressions like That said I think the language team should just pick a route since I do not believe the community will come to a consensus here on the syntax. |
...Or just simply "out". An example using return switch (this) {
// ...
Lambda(:final formals, :final body)=>.(
//... Some code
out x;
),
// ...
}
// in collection literals:
var list = [
first,
second,
if (cond) .(
...
out x;
)
]; There can be a better symbol than dot, but parentheses (instead of |
Switch expressions are really useful, however, In the past few days of working with them frequently I've run into the problem where most of my bodies of the cases are expressions, but I need a proper body for the minority of them. In such cases I have to revert everything to a switch statement (it's good that there is an assist for it), or I have to create an immediately invoked function. This is poor user experience.
My proposal would be to allow a body instead of an expression in switch expressions. The body has the caveat that it cannot use a break / continue, and has to have a return statement. So the previous example would change to this: The semicolon of the return statement then separates the cases.
The text was updated successfully, but these errors were encountered: