Open clsource opened 3 years ago
As far as option 2 is concerned, I'm not sure whether we've decided to use case else
to save adding the default
keyword. Personally, I'm fine with either.
An idea, thought it may bring some confusion, instead of adding a falltrought
keyword, maybe continue
may be reused here ?
Reading it with some delay, it made me realize that option 1 looks more what shell case
does.
I'd prefer not to use continue
for fallthrough because we won't then be able to use it to continue a loop enclosing the switch
statement.
I suppose we could use a hybrid such as case continue
but, personally, I'd prefer to use fallthrough
(Go and Swift use it) whose meaning is absolutely clear and is unlikely to have been used much as an identifier in the past.
Any thoughts on my suggestion above that we do ==
first and then consider expanding the syntax and semantics to other types of comparison? If we are careful up front, we won't lock ourselves out of other options. https://github.com/wren-lang/wren/issues/956#issuecomment-817337864
Actually, I've had a further thought about continue
.
One thing we haven't talked about so far is whether one should be able to break from the switch
statement prematurely before the end of the current case is reached. On the face of it, there seems no reason not to allow this - other languages (including C) do - and the natural keyword to use would be break
.
Now, if we did that, then logically continue
would be fine for fallthrough.
Moreover, if #964 were to be adopted as well, then we could still break from or continue enclosing loops by the simple device of tagging those loops.
So I think overall that would be a nice solution, which would marry the two proposals and avoid the need for a new fallthrough
keyword.
@cxw42 I think we've reached the stage now where we need to hear from @ruby0x1 which, if any, of the options she thinks is worth pursuing before attempting a preliminary PR or trying to develop that option further.
@PureFox48 I agree this is a good time to pause for review. Just so I'm not missing anything, are we asking for review of only the three basic syntax options in the side-by-side? Or also other features?
Sorry I wasn't clear in my last comment --- what I was trying to say is that I think a review of the side-by-side would be good, with other features to be added later.
It might be helpful to extend your table to show how the three options would cater for some basic pattern matching in addition to simple value equality. I think the ability (or at least the potential) to do this is essential for a switch
to be worth doing in the first place.
Subject to that, @ruby0x1 should be able to get a feel from the side-to-side which one of the options would fit Wren the best.
@PureFox48 I'll see what I can do - I just worry about combinatorial explosion in the number of options. Would you be willing to summarize the pattern-matching ideas from the thread? Then I'll work them into the side-by-side.
Looking for inspiration here is Dart's Switch https://dart.dev/guides/language/language-tour#switch-and-case
Dart Example 1
var command = 'CLOSED';
switch (command) {
case 'CLOSED': // Empty case falls through.
case 'NOW_CLOSED':
// Runs for both CLOSED and NOW_CLOSED.
executeNowClosed();
break;
}
Dart Example 2
// If you really want fall-through, you can use a continue statement and a label:
var command = 'CLOSED';
switch (command) {
case 'CLOSED':
executeClosed();
continue nowClosed;
// Continues executing at the nowClosed label.
nowClosed:
case 'NOW_CLOSED':
// Runs for both CLOSED and NOW_CLOSED.
executeNowClosed();
break;
}
// A case clause can have local variables, which are visible only inside the scope of that clause.
So maybe here are some ideas:
case
to for
to save a keywordtags
for fallthoughtvar command = 'CLOSED';
switch(command) as value {
for ("UNKNOWN"): // empty (no newlines or spaces after : ) is auto fallthought
for ("CLOSED"):
executeClosed()
continue nowClosed // fallthought
for ("NOW_CLOSED") as nowClosed:
executeNowClosed()
// breaks are implicit
for (value.count > 6): // pattern matching?
executeBigCommand()
for else:
// default case
}
Would you be willing to summarize the pattern-matching ideas from the thread?
Here's a list for Option 2:
switch (x) {
case 1..5: // range of values
case 6..10 if (x % 2)== 0: // range with guard
case 6, 8, 10: // list of values
case Num: // type of value
case if x > 12: // open ended range
case if x != 11: // excluded value
case if x >= 13 && x <= 20: // closed range (non-integral)
case if x + y == 16: // using another variable
case if x.isEven: // method call with 'x' as receiver
case if Int.isOdd(x) // method call with 'x' as parameter
}
The corresponding ones for Option 1 are:
switch (x) {
in 1..5: // range of values
in [6, 8, 10]: // list of values
is Num: // type of value
> 12: // open ended range
!= 11: // excluded value
.isEven: // method call
}
Note that I'm using an operator in
for a couple of these which doesn't currently exist though I'm thinking of opening a RFC for it.
The ones that are missing compared to Option 2 would probably be handled using ~~
followed by a function. I'd be inclined to exclude those from the comparison for now as they haven't been fully discussed yet.
Perhaps you could fill in Option 3 yourself as I don't think there are any examples in the thread so far.
I think case if <condition>
can be replaced with case <variable> if <condition>
, for example case x if x > 12
so that you can bind the expression if it is complex.
I'm not sure what to make of Dart but it looks like a traditional switch
with no automatic fallthrough but which nevertheless requires a break
statement at the end of each case. You can jump amongst cases using continue
followed by a label.
Rather than introduce an Option 4 at this late stage, could we just regard using for
rather than case
as a sub-option of Option 2.
@ChayimFriedman2
Yes, I think I said earlier in the thread that's effectively what it would be - the compiler would add the preceding 'x' before the if
if you didn't specify it explicitly.
So you always get x
?
That's a terrible name.
Well, it would make use of @mhermier's idea of using a tag for a complex expression:
switch (expr) as x {
case if x > 12: // implicit
case x if x > 12: // explicit
}
'x' , of course, could be any valid identifier.
I just didn't see any as
in your expression. Either way, I prefer binding in the case
clause, but maybe I'm just influenced by Rust, where nested patterns can have bindings š
@clsource I'm not a big fan of Dart's switch, myself, but I agree with the point about consecutive labels --- that's something I hadn't thought about.
@ChayimFriedman2 the variable name has to either be a keyword or selected by the programmer in the code, as far as I can tell, because the argument to switch
could be a nameless temporary.
@PureFox48 but as x
here conflicts with #964. The same syntax means something completely different. (Edit and I see that we are in agreement on that :) --- https://github.com/wren-lang/wren/issues/964#issuecomment-817324828)
I'm a believer that similar things should look similar, and different things should look different. If we really want as x
, let's go with switch(expr as x) {...}
here and change #964 to LABEL: for(expr) {...}
(https://github.com/wren-lang/wren/issues/964#issuecomment-814250544) or one of the other options.
all: Just to be clear, the flow-control semantics for this initial check with @ruby0x1 are the same for all the options, right?
Breaking out of the middle of a case (https://github.com/wren-lang/wren/issues/956#issuecomment-817736135) is TBD
I just didn't see any as in your expression.
That's because there wasn't one. I was just assuming that 'x' was a variable for simplicity :)
There isn't in fact any conflict with #964 because the tag there would be on the loop. Here it's on the switch
.
I suspect he meant that we could also want to break out of a nested switch
. But that starts becoming more and more goto
-y.
Re. as x
--- But then the programmer has to remember that as x
means "value" in one case and "loop label" in the other. It's like all the things that static
can mean in C --- they are easy for the experienced programmer, but a real stumbling block for the beginner.
If we are going to name values, I would suggest uniformly supporting Edit 2 I thought about it some more and realized if(var foo=expr)
, while(var foo=expr)
, and switch(var foo=expr)
.(var foo=expr)
really only makes sense for switch
, since almost everything in Wren is truthy. Please disregard this comment!
Then the question is - who is the person we are optimizing programming languages for? I think this is the professional programmer.
But I think we start to discuss not important things. Let's let @ruby0x1 decide what's the direction, then think about it more.
Well, the alternative would be to use:
var x
switch (x = expr) {
case if x > 12: // implicit
case x if x > 12: // explicit
}
@PureFox48 I much prefer that to overloading as x
:+1:
Just to be clear, the flow-control semantics for this initial check with @ruby0x1 are the same for all the options, right?
Yes, I think so.
@ChayimFriedman2
who is the person we are optimizing programming languages for?
As far as I can tell, all of us in this discussion are experienced programmers. We are certainly part of the target audience :) .
My impression of Wren is beginners are also part of the target audience. Wren describes itself as a new take on Lua's application space ("Think Smalltalk in a Lua-sized package"). Lua has been successful largely because it is accessible to beginners (4.6M Google hits for "lua programming language for beginners" as I type this).
And, anyway, if it's easier for beginners, it will probably have lower cognitive load for us, too :D .
@PureFox48
switch(x = expr)
I think that would make the parser much easier than the option-1 choices you kindly listed in https://github.com/wren-lang/wren/issues/956#issuecomment-818311465 (thanks for putting that together!). The parser could, for each case:
<variable> ==
on the front of the expression, and use thatE.g.,
switch (var x = expr) {
case 3: // parser doesn't see an `x`, so it emits `x == 3` (adding the `x ==` part transparently to the user)
case x > 12: // parser does see an `x`, so it emits `x > 12`
// Leave this one out --- don't need two names for the same value // case x if x > 12: // explicit
}
The same idea would work in any of the three syntax options.
@cxw42
TBH I hadn't considered nested switch
before but if switch
is to support break
and continue
independently in the way I suggested earlier, then you're right that we have to be able to tag switch
as well as loops and can't therefore entertain using the tag as a variable alias instead.
My last thought for the day --- I think we should agree on a short-term goal. @PureFox48 I understand and appreciate that you need what I'll call "complex conditions" (i.e., anything other than equality comparison) in your use cases. My use cases do not need complex conditionals, though I'll certainly use them once we have them :) .
all: I suggest we choose one of the following short-term goals:
switch
without complex conditionals; orswitch
with complex conditionals.In case (1), basic switch
, I think we can meaningfully ask @ruby0x1 to review the side-by-side and give us her thoughts on syntax. We would then work on complex conditionals once basic switch
was implemented.
In case (2), complex conditionals, I think we should spend some more time on the design before we ask @ruby0x1 to review. Given that we're still coming up with ideas :) , it doesn't seem to me ready for review.
What say you all?
Biggest issue is that if we always think short in grammar, at some point we fill fail hard because of some non anticipated consequences, which will requires crazy expressions, extra keywords or some library to be solved.
The as
syntax represent an alias/variable declaration to some value as per import
. All the execution flow expressions like while
, if
, switch
... compute a value that is usually thrown away. While I agree the keyword(var x = ...)
is nice, the keyword(...) as x
can be generalized in all those constructions, and give the benefit of providing a tag for control flow in switch.
The only problem I see with as
is that the import
grammar precedence seems to implies that the name survives after the expression...
So I think maybe we should sort the assignment first (without ignoring the knowledge we built), which will give the direction to resolve #964 and this one.
@cxw42
Although they haven't been fully explored yet, I think it's clear that all 3 options should be able to deal with complex conditionals to some extent or other.
I'd therefore be on board with asking @ruby0x1 to review these options from the viewpoint of a basic switch
to keep things simple for now.
After reviewing the latest batch of points, I think there is a problem with nested switch
and the use of continue
for fallthrough. This problem stems from the requirement that fallthrough
needs to be the final statement in a case
clause which Go and (apparently) Swift insist upon. @mhermier, who is probably looking ahead to a possible implementatiion, also said earlier in the thread that he thought this was necessary.
Consider this example (using Option 2 syntax):
switch (x) as outer {
case 1:
switch (y) {
case 1:
continue outer // error - there's another statement afterwards
case 2:
// other cases
}
doSomthingElse() // fly in the ointment
case 2:
// other cases
}
Unless, the nested switch
happens to be the last statement in an outer switch's case
clause, continue outer
is going to error out.
I can think of two possible solutions here:
We allow it anyway.
We disallow using continue
with a switch tag (but not a loop tag) altogether.
Personally, I'm inclining towards the first of these in the interests of generality.
A minor but awkward point which will need addressing if switch
goes ahead.
@PureFox48, I think it is a fair point, and that will need proper testing.
@PureFox48 I think that is a reason to go back to using a different keyword for fall-through. Also, continue
is currently always a backwards jump, and fallthrough
is always a forward jump. I think those are different enough that another keyword wouldn't hurt.
@mhermier
Biggest issue is that if we always think short in grammar, at some point we fill fail hard because of some non anticipated consequences, which will requires crazy expressions, extra keywords or some library to be solved.
I agree this is a risk. I am also concerned about the risk of doing a full design and then having to start over because we missed something up front.
Do you have specific concerns with the three options in the side-by-side that we could address now, and then still request an early review? If we could mitigate both risks, that would be great!
(Reminder for those joining the thread: The three options are, for each case in a switch
:
<expr>: <stmt>|<block>
case <expr>: <stmts>
(<expr>) <stmt>|<block>
)
I think that is a reason to go back to using a different keyword for fall-through.
Yeah, you may be right. It's what Go and Swift do even though continue
is available to them.
I have decided that I like the requirement for less typing in option 1 :) . I am willing to support switch
using the option-1 syntax.
I am willing to support switch using the option-1 syntax.
...and then there were two (Agatha Christie?).
I started off as an Option 1 supporter before drifting across to Option 2. But I'd be happy with either :)
I vote for option 1. I dont like case keyword xd
I have started a smartmatch-based, option-1 switch
implementation at https://github.com/cxw42/wren/tree/switch, just to see how it might go. It now compiles the following, but executes both cases :) ---
switch(1) {
1: System.print("one")
else System.print("nope")
}
The Wren codebase is very enjoyable to work with! :D
May you remove all of the whitespace diffs? It makes it impossible to review.
The Wren codebase is very enjoyable to work with! :D
Indeed š
@ChayimFriedman2 now updated and functional! I haven't undone the whitespace changes yet, but you can diff without them --- https://github.com/wren-lang/wren/compare/main...cxw42:switch?w=1 Edit Note: it is not yet cleaned up and PR-worthy. I certainly welcome any early thoughts!
@cxw42 don't add DUP, revert its removal 001db02c817c201f1fff4b049a0395ad493061c6
@mhermier done, and added some more tests and some complex conditionals.
@cxw42 Some parts are still not elegants/too much noisy:
Code instruction = CODE_CALL_0;
seems instruction is not used.@cxw42 rethinking at it maybe we should go something hybrid like this:
switch(a) {
1: System.print("one") // literal => a == 1
"42": System.print("the answer") // literal => a == "42"
== 1: System.print("how ? ") // explicit binaryop equality => a ~~ "abcdefg"
~~ "abcdefg": System.print("so nice") // binaryop => a ~~ "abcdefg"
.isInteger.not: System.print("") // function call => a.isInteger.not
else System.print("nope")
}
It would be less surprising for new users, while allow more complex behaviors for cheap.
Thought it would require case
, because of the next line .
look ahead.
@mhermier We discussed the hybrid approach above, and it does have some advantages. How would you distinguish unary +
from binary +
at the beginning of a case? E.g.,
switch(a) {
+1: ... // should match a == +1? Or should match if (a+1) is truthy?
+1 != 42: ... // should be interpreted as (a+1) != 42? How do you know when the parser is at the `+` what to do?
}
@mhermier
Some parts are still not elegants/too much noisy
That's why I haven't opened a PR yet :) . Thanks again for the early feedback --- I will address your comments before opening a PR.
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