Open clsource opened 3 years ago
Thought about an extra stuff, following while loop, maybe it would be interesting to do something like:
switch (my_long_expression) as myVariable {
// what ever syntax here we have access to the switch value as myVariable in the block
}
// myVariable is now undefined...
: would need to be searched to determine if the expression is a case statement starting with a binary expression or a real statement starting with a unary expression.
I don't entirely follow that because the case could not begin with a unary expression anyway. The only unary operators we have are: -
, !
and ~
and none of those could be used in isolation. A binary operator is needed to refer back to the switch expression.
In my experience, fallthrough is almost always not needed. If it is, you can always fall back to if-else. So I tend towards mandating the break, except for adjacent cases.
I'm struggling to follow your logic re fallthrough here. I agree that it's seldom needed and one of the things I find tedious about programming in C is that you have to remember to put break
at the end of each case clause to avoid it from falling through. This in turn means that you can't then use break
to exit a loop enclosing a switch.
Surely, it's best for Wren not to allow automatic fallthrough (the 'majority' case) so break
can then be freed to fulfil its normal function of exiting loops.
Regarding requiring a default clause, I don't think this is a good idea.
Well, it's been a long thread and I may have missed something but I don't think anyone has suggested that we should require a default clause. I certainly don't think we should.
I'm struggling to follow your logic re fallthrough here. I agree that it's seldom needed and one of the things I find tedious about programming in C is that you have to remember to put
break
at the end of each case clause to avoid it from falling through. This in turn means that you can't then usebreak
to exit a loop enclosing a switch.Surely, it's best for Wren not to allow automatic fallthrough (the 'majority' case) so
break
can then be freed to fulfil its normal function of exiting loops.
I agree, I just want to keep the break
for C familiarity.
Well, it's been a long thread and I may have missed something but I don't think anyone has suggested that we should require a default clause
Oh, you're right. I was confused by:
To avoid potential confusion, perhaps we could mandate that the
else
case, if there is one, always comes last in theswitch
.
Skipped "if there is one" 😄.
I don't entirely follow that because the case could not begin with a unary expression anyway. The only unary operators we have are:
-
,!
and~
and none of those could be used in isolation. A binary operator is needed to refer back to the switch expression.
You are assuming the future here, we might introduce more operators that might conflict with your statement here. I spoke about the case where the code is something like this:
switch(expr0) {
test:
expr1
expr2
expr3
}
If we don't put brace around expr1/2/3, the whole expressions need to be parsed to see that there is no terminal :
to determine if it is an expression (because of unary operators and the continuation of expression with .methodName
in next line)
To summary, I only see 2 viable witting style:
switch(expr0) {
test1: one_liner_expr
test2: {
multiline_exprs
}
else // `:``is optional here followed by one liner or multiline expressions in a block
}
or
switch(expr0) {
case test1: oneliner_expr
case test2:
multiline_exprs
default // `:``is optional here, but `else` can't really be used because it could be mixed with multiline expressions
}
case
and default
can be replaced with anything.
First syntax introduce only 1 symbol while second one requires at worse 3 (not counting required fallthrough
or break
keyword that is still debated).
Personally I prefer first one, it will introduce less keywords and looks more like the rest of the wren syntax regarding one liners and blocks.
Yes, I think you're right that we're left with basically 2 viable styles.
Like it or not, I suspect the deciding factors here will be:
The need for blocks which I don't think most commenters like given that they're not generally needed in other C-family languages.
Familiar syntax.
As you don't think your own approach can work without blocks for multi-line cases, I'm coming to the conclusion that we need to make the more traditional switch/case
syntax work - perhaps on the lines of @ChayimFriedman2's latest approach but (please!) with no automatic fallthrough.
We could get by with 2 new keywords (switch
and case
) by calling the default case case else
. If we're to allow fallthrough, I personally would prefer that to be the keyword but could live with proceed
instead.
Incidentally, I think your idea of a variable alias:
switch (my_long_expression) as myVariable {
// blah
}
is a good one, particularly in the context of guard
clauses.
Just BTW, VB has Case Else
.
Thinking at the switch (my_long_expression) as myVariable
for the sake of uniformity maybe it should also be extended to 'if' and also to all loops.
@ChayimFriedman2
I've been studying your latest approach.
Presumably, if guard
clauses could be made to work, then we could cater for more general expressions involving 'x' and perhaps other in scope variables as well or am I being over-optimistic.
For (very silly) example:
switch (x) {
case x > 7: // open ended range
case x != 9: // exclusive value
case x >= 3 && x <= 10: // closed range (non-integral)
case x + y == 6: // using another variable
case x.isEven: // method call
case Int.isOdd(x) // method call with 'x' as parameter
case else: // default case
}
BTW, in C (and similar languages) complicated conditions can be represented using the following trick:
switch (true) {
case x > 7: // open ended range
case x != 9: // exclusive value
case x >= 3 && x <= 10: // closed range (non-integral)
case x + y == 6: // using another variable
case x.isEven: // method call
case Int.isOdd(x) // method call with 'x' as parameter
default: // default case
}
I don't like this trick, but there are others that do.
Edit: It turns out that it does not work in C, but it does in other languages.
I don't like, even if polished, it brings no adventages over regular if
. Would the following corrected would make sense ?
switch (expr) as x {
case > 7: // open ended range
case != 9: // exclusive value
case >= 3 && x <= 10: // closed range (non-integral)
case + y == 6: // using another variable
case .isEven: // method call
case .isOdd: // method call with 'x' as parameter
else: // default case
}
And then how do you perform a method call with expr
as argument (not receiver)?
Using the trick I explained ealier using a method/operator in object, or as @cxw42 named it later '~~'.
switch (expr) as x {
case ~~ MyUnaryFunction: ...
case ~~ Fn.new {|foo| ...}: ... // Edited for clarity that it is invoked with an argument
case ~~ Fn.new { /* use x directly*/ ...}: ...
...
}
It would be better than regular if/else
if the complex expressions could co-exist with very simple expressions in the same statement.
Personally, I don't really see how you can have complex expressions without repeating 'x' (or whatever) as it just doesn't look right.
It is interesting in the general case, since usually it is either a value equality check or a property test. For complex expression, I think it start to be interesting when you start to think functional and reusable.
If we go the route you propose, it means that the as
become mandatory, meaning we could replace it with something almost equivalent:
with (expr) as x {
if (x > 7) // open ended range
else if (x != 9) // exclusive value
else if (x >= 3 && x <= 10) // closed range (non-integral)
else if (x + y == 6) // using another variable
else if (x.isEven) // method call
else if (Int.isOdd(x)) // method call with 'x' as parameter
else ... // default case
}
And complete equivalence can be achieved by adding fallthrought
to if
.
Are you talking there about a possible implementation for switch/case
or adding a with
statement as well or instead of it?
I mean if we provide full complexity, it only replace else if
with a fancier case
...
Well, leaving aside possible compiler optimizations, that's all switch/case
really is - syntax sugar for an if/else
ladder based on the value of a single expression, albeit more elegant.
Well I see it more like a way to perform a test without the explicit need to named a variable, removing repeat of it XX times. This brings some interest to switch
. If the named variable is required all the times, it add no value over the if/else
ladder.
Yes, I agree that not having to repeat the switch variable is one of the things that makes it more elegant.
But, I'm not suggesting here that we repeat 'x' all the time - just on those occasions when a complex condition is needed as well as simple ones.
Maybe we can make an hybrid, but it depending if we have block imposed or not, add an extra keyword, by doing something like:
switch(expr) as x {
case 1:
case 2:
other_keyword x + y > 42:
}
But I don't really like it...
You could use case if
for complex expressions.
It would be like a guard
but without any preceding expression.
switch(expr) as x {
case 1:
case 2:
case if (x + y) > 42:
case else:
}
While it works, I think it become a burden in case of consecutive matching by other complex mean than equality.
If we had method resolution or something we could provide a secondary argument to change the meaning of case
to something like:
switch (expr, .~~(_)) as x {
case foo: // Would perform: x~~(foo)
...
}
I don't think it would become any more of a burden in the case of consecutive matching than an if/else
ladder.
There is no need for cases to be mutually exclusive and the first case to be satisfied would be the one executed.
If guards
were in place, the compiler could replace case if
with:
switch(expr) as x {
case x if (x + y) > 42:
case else:
}
Re. @mhermier https://github.com/wren-lang/wren/issues/956#issuecomment-817302284:
Thanks for the code example! I agree the expression and the statement need to be separated by the parser unambiguously. A keyword can do that, or parens, or any other symbol. There just needs to be something that's not valid in an expression after the expression, e.g., a colon or a terminal right paren. I think the keyword at the front is optional, since the parser will know what it's looking for.
Re. @PureFox48 https://github.com/wren-lang/wren/issues/956#issuecomment-817316026:
The need for blocks which I don't think most commenters like given that they're not generally needed in other C-family languages.
I don't want Wren to look like C! I want it to look like Wren. I understand not everyone agrees with me :) . Wren already has well-defined one-liner and block syntax. I suggest that it's more important to be able to go easily from Wren's if
to Wren's switch
than from C's switch
to Wren's switch
.
Well, Wren's if
and while
statements are exactly the same as in C. Only the for
statement differs.
@mhermier and I were discussing earlier what makes switch/case
more elegant than an if/else
ladder. As well as not usually having to repeat the switch
variable, I would humbly suggest that not having to use blocks would be another aspect.
Adding to the list from @mhermier https://github.com/wren-lang/wren/issues/956#issuecomment-817313384 , I propose option 3 (what I said above, but in a compact form):
switch(expr0) {
(test1) one_liner_expr
(test2) {
multiline_exprs
}
/* optional feature --- not needed in minimum viable product */
{|v| v.someFn() } <statement> // statement can be oneliner or {\n block\n }
else <statement>
}
Pro: Only one additional keyword (plus whatever we pick for fallthrough), and we can re-use existing parser logic that handles what comes after an if
or while
keyword for each individual case.
Con: doesn't look like C (but I don't think it needs to)
I support fallthrough being opt-in, not the default.
As you know, I like pattern-matching/smartmatch :) . Would it be worth discussing those later? Even if we only had ==
tests, switch
would still make a big difference. I suggest we narrow the scope of this issue to ==
until we agree on syntax and semantics for the ==
case. What say you?
@PureFox48 Would you please tell me more about the downsides of blocks in your experience? How do you see those downsides translating to Wren?
I'm talking purely about aesthetics here and trying to add some value to switch/case
compared to if/else
.
To my mind, blocks make the switch
statement look more cluttered and not that much different than an if/else
ladder. May be it's just me but that's how I see it.
Fair enough --- thanks for the explanation! I agree to disagree :)
@cxw42 grammatically wise your option is almost equivalent to case 1, with some minor twist to evaluation like the last development. I find it too much confusing because basically it transform "expr expr" as an if
statement, which not something we really want in a grammar...
@PureFox48 true, but this is how wren manage multi-line in if
in wren... edit well in general, it is also true for methods.
Perhaps I should make it clear that I've no problem with blocks when they're necessary.
However, if they're not necessary (and I don't think they are in traditional switch/case
syntax) then I'd prefer to dispense with them on aesthetic grounds.
@mhermier
almost equivalent to case 1
Yes, on purpose :) . Option 3 is just option 1, but with parens instead of case
and :
.
basically it transform "expr expr" as an if statement, which not something we really want in a grammar...
I'm not sure I understand what you mean here. Let me try this --- I am thinking of BNF something like:
switch_stmt ::= 'switch' '(' <expr> ')' '{' <cases> '}'
cases ::= <case>+
<case> ::= 'else' <stmt> /* has to be the last <case>, if it exists */
| '(' <expr> ')' <stmt>
where <stmt>
is Wren's regular statement production, and covers both single-line and multi-line.
Anyway, if case <expr>:
turns out to be the winner, I'll still use it :) . I just think it will face an uphill battle, given comments I've seen from the maintainers on other issues.
I'll selectively quote Bob from Crafting Interpreters ---
Do you start from a blank slate and first principles? Or do you weave your language into the rich tapestry of programming history and give your users a leg up by starting from something they already know?
There is no perfect answer here, only trade-offs.
Edit I am very glad that we are working through the trade-offs! I do think switch
will be a big win, whatever it winds up looking like.
I think, the following would be good for me:
switch (expr[, optionalEqualityCallable]) as x {
case foo: // Would perform: optionalEqualityCallable.call(x, foo)
...
case if expr:
case else: // or default which is more natural
}
It should work for most common cases, and let the user customize the notion of equality for case
. Like transforming case string literal to do pattern matching, or any other kind of transform required to perform an equality.
The idea of a custom equality checker is an interesting one --- lighter weight than full smartmatch, and more flexible than just ==
.
If we are going to use as x
, why outside the parens? That conflicts with our proposal in #964. And, if you do include optionalEqualityCallable
, the x
could be very far from where it's used. What about---
switch([var x = ]<expr>[; optionalEqualityCallable]) {...}
where x
's scope is the scope of the switch
statement. (Sorry if this was mentioned above and I missed it!)
(Edit I'm stepping away from the console for a while. I'm going to ask @ruby0x1 on Discord to comment on whether case <expr>: ... break;
would be acceptable. I suspect it won't be since it adds a new compound-statement form, but I could well be wrong.)
@cxw42 I would have loved to be able to do that, but there is a strange feature from if
that take precedence, that is bugging me since wren creation.
if (someNewVariableName = 42) { ... }
For me is inconsistent, since it creates a new variable if the variable was not defined earlier. Instead it should be an error in a regular bloc/function, and result in using a setter in a method. This would be more logical.
@mhermier agreed --- the docs say "Wren doesn’t roll with implicit variable definition", so it shouldn't be allowed in an if
statement. Have you opened an issue for that? I don't see one.
If there is, it was decided so long ago it was decided/resolved by @munificent himself.
Sorry, if I'm missing something, but this doesn't compile for me:
if (someNewVariableName = 42) System.print(someNewVariable)
I get: Error at 'someNewVariableName': Variable is used but not defined.
@mhermier it looks like that might have been fixed. I just tried it in wren_cli
---
> if(w=3) System.print("yes")
[repl line 1] Error at 'w': Variable is used but not defined.
edit @PureFox48 you beat me to it :laughing:
Yeah, it looks like all is well after all :)
@ruby0x1 says
I'm letting the thread play out atm. since it's 50+ notifications when I woke up today, for me it's better to just let all the thoughts be explored and when it makes sense to respond, do so holistically
Now I actually am going to log off for now :D
Seems that it is not how I remembered. Anyway the as
or var
is optional to the declaration and can be resolved in another issue.
All of this long discussion made me realize how hard it is to bake switch
into Wren.
I think we should instead focus on making if
-else if
-else
chains better.
One thing that I always hated is that without braces you cannot really use if
-else
. The following is invalid in Wren:
if (1 == 0) System.print("Hah??")
else System.print("OK.")
Instead it should be:
if (1 == 0) System.print("Hah??") else System.print("OK.")
Which makes it unusable for if
-else if
-else
chains.
I suggest we change this to allow the else
on another line. This is very easy to fix (something I can do in some minutes), yet very useful and much less controversial than switch
.
I wish you'd said this 90 odd posts ago :)
Although I'd personally welcome such a change, it wouldn't alter the fact that switch/case
can be much more elegant than if/else
ladders otherwise languages wouldn't bother with it at all.
One further point is that if we are to have a switch
statement, it would be best for it to be introduced sooner rather than later because, as time passes and more and more Wren code is written, it becomes more difficult to introduce new keywords without breaking a lot of existing code.
While I understand the motivation I'm not sure it helps for readability, But this is another topic for another issue.
And the topic of switch
is one of the most interesting exchange for wren I had in a long time. I think it is a good addition, by conformance to other languages and ease of write. Even if there is no concencus/adoption at the end, all that effort is not wasted. It makes people think on language design and make them dig/learn the code.
Below is a side-by-side comparison of a a real-world if/else-if chain and the three options as I currently understand them. The source files are in the above-linked commit if you want to see where this came from. The original code is here in wren-toml. I hope this helps!
// Original - from wren-toml // option 1 as I understand it // option 2 as I understand it // option 3
if (TOMLScanner.isAlphanumeric(char)) { if (TOMLScanner.isAlphanumeric(char)) { if (TOMLScanner.isAlphanumeric(char)) { if (TOMLScanner.isAlphanumeric(char)) {
scanAlphanumeric() scanAlphanumeric() scanAlphanumeric() scanAlphanumeric()
} else if (TOMLScanner.isWhitespace(char)) { } else if (TOMLScanner.isWhitespace(char)) { } else if (TOMLScanner.isWhitespace(char)) { } else if (TOMLScanner.isWhitespace(char)) {
// No-op. // No-op. // No-op. // No-op.
} else if (char == "=") { } else switch(char) { } else switch(char) { } else switch(char) {
addToken(Equal) "=": addToken(Equal) case "=": addToken(Equal) ("=") addToken(Equal)
} else if (char == "\"") { "\"": { case "\"": ("\"") {
if (peek() == "\"") { if (peek() == "\"") { if (peek() == "\"") { if (peek() == "\"") {
advance() advance() advance() advance()
elidedMultiline() // removed for brevity elidedMultiline() elidedMultiline() elidedMultiline()
} else { } else { } else { } else {
scanString() scanString() scanString() scanString()
} } } }
} else if (char == "#") { } case "#": scanComment() }
scanComment() "#": scanComment() case "[": addToken(LeftBracket) ("#") scanComment()
} else if (char == "[") { "[": addToken(LeftBracket) case "]": addToken(RightBracket) ("[") addToken(LeftBracket)
addToken(LeftBracket) "]": addToken(RightBracket) case "'": scanString("'") ("]") addToken(RightBracket)
} else if (char == "]") { "'": scanString("'") case ",": addToken(Comma) ("'") scanString("'")
addToken(RightBracket) ",": addToken(Comma) case "{": addToken(LeftBrace) (",") addToken(Comma)
} else if (char == "'") { "{": addToken(LeftBrace) case "}": addToken(RightBrace) ("{") addToken(LeftBrace)
scanString("'") "}": addToken(RightBrace) default throwScannerError() ("}") addToken(RightBrace)
} else if (char == ",") { else throwScannerError() } else throwScannerError()
addToken(Comma) } }
} else if (char == "{") {
addToken(LeftBrace)
} else if (char == "}") {
addToken(RightBrace)
} else {
throwScannerError()
}
If I have misunderstoood option 1 or option 2, please let me know!
(note: I picked this code sample by browsing through the ecosystem modules listed in the wiki. This was the first complex if/else chain I found.)
My personal order of preference is option 3 first (still), then option 1, then option 2. Any of them is a significant improvement! I noticed that option 1 is the most concise by number of characters typed, which is part of why I prefer it over option 2.
Lastest devellopement the header can be changed to:
if (TOMLScanner.isAlphanumeric(char)) {
scanAlphanumeric()
} else if (TOMLScanner.isWhitespace(char)) {
// No-op.
} else switch(char) {
...
can be compressed to:
switch(char) {
case if TOMLScanner.isAlphanumeric(char): scanAlphanumeric()
case if TOMLScanner.isWhitespace(char): { } // No-op. or break or whatever
...
I think it is generalisable with and without case
.
And if equality argument is accepted to, with a little bit more of functionnal programing:
var myCustomEquality = Fn.new {|lhs, rhs|
if (rhs is Fn) return rhs.call(lhs)
return lhs == rhs
}
switch(char, myCustomEquality) {
case TOMLScanner.isAlphanumericFn: scanAlphanumeric()
case TOMLScanner.isWhitespaceFn: { } // No-op. or break or whatever
...
It would require to add a
switch
keyword. Butcase
anddefault
can be replaced with other available symbols like|
andelse
. It has implicit return.Other alternatives to
else
as suggested by @CrazyInfin8 :_
,default
,otherwise
. But it would probably be needed another keyword. Ifelse
is not preferred my vote would be for_
(underscore).Another way without adding a new keyword.
if
andis
Another syntax variation