wren-lang / wren

The Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.
http://wren.io
MIT License
6.91k stars 552 forks source link

[RFC] Switch Statement #956

Open clsource opened 3 years ago

clsource commented 3 years ago

It would require to add a switch keyword. But case and default can be replaced with other available symbols like | and else. It has implicit return.

Other alternatives to else as suggested by @CrazyInfin8 : _, default, otherwise. But it would probably be needed another keyword. If else is not preferred my vote would be for _ (underscore).


// simple
switch (value) {
    | 1 : true,
    | 2 : false,
    | else : Fiber.abort()
}

// multiple instructions per case
switch (value) {
    | "green": 
        myfunc.call()
        myfunc2.call()
        return true
    | 2: 
        myfunc2.call()
        return myClass.new()
    | else:
        return Fiber.abort()
}

// fall throught
switch (value) {
  | 1 : continue
  | 2 : true // if the value is 1 or 2 would return true
  | 3 : Fiber.abort()
  | else : false
}

Another way without adding a new keyword. if and is


// simple
if (value) is {
    | 1 : true,
    | 2 : false,
    | else : Fiber.abort()
}

// multiple instructions per case
if (value) is {
    | "green": 
        myfunc.call()
        myfunc2.call()
        return true
    | 2: 
        myfunc2.call()
        return myClass.new()
    | else:
        return Fiber.abort()
}

// fall throught
if (value) is {
  | 1 : continue
  | 2 : true // if the value is 1 or 2 would return true
  | 3 : Fiber.abort()
  | else : false
}

Another syntax variation


// simple
if (value) is {
    | 1 | : true
    | 2 | : false
} 

// simple (mutiple cases combined)
if (value) is {
 | [1, 2, 3, 4] | : true // value is 1 or 2 or 3 or 4
 | > 4 | : false
} else {
  Fiber.abort()
}

// multiple instructions per case
if (value) is {
    | "green" | : 
        myfunc.call()
        myfunc2.call()
        return true
    | 2 | :
        myfunc2.call()
        return myClass.new()
} else {
   Fiber.abort()
}

// fall throught
if (value) is {
  | 1 | : continue
  | 2 | : true // if the value is 1 or 2 would return true
  | 3 | : Fiber.abort()
} else {
  return false
}
PureFox48 commented 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.

mhermier commented 3 years ago

An idea, thought it may bring some confusion, instead of adding a falltrought keyword, maybe continue may be reused here ?

mhermier commented 3 years ago

Reading it with some delay, it made me realize that option 1 looks more what shell case does.

PureFox48 commented 3 years ago

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.

cxw42 commented 3 years ago

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

PureFox48 commented 3 years ago

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.

cxw42 commented 3 years ago

@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.

PureFox48 commented 3 years ago

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.

cxw42 commented 3 years ago

@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.

clsource commented 3 years ago

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.

Ideas

So maybe here are some ideas:

var 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 
}
PureFox48 commented 3 years ago

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.

ChayimFriedman2 commented 3 years ago

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.

PureFox48 commented 3 years ago

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.

PureFox48 commented 3 years ago

@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.

ChayimFriedman2 commented 3 years ago

So you always get x?

That's a terrible name.

PureFox48 commented 3 years ago

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.

ChayimFriedman2 commented 3 years ago

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 šŸ˜„

cxw42 commented 3 years ago

@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

PureFox48 commented 3 years ago

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 :)

PureFox48 commented 3 years ago

There isn't in fact any conflict with #964 because the tag there would be on the loop. Here it's on the switch.

ChayimFriedman2 commented 3 years ago

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.

cxw42 commented 3 years ago

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.

cxw42 commented 3 years ago

If we are going to name values, I would suggest uniformly supporting if(var foo=expr), while(var foo=expr), and switch(var foo=expr). Edit 2 I thought about it some more and realized (var foo=expr) really only makes sense for switch, since almost everything in Wren is truthy. Please disregard this comment!

ChayimFriedman2 commented 3 years ago

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.

PureFox48 commented 3 years ago

Well, the alternative would be to use:

var x
switch (x = expr) {
    case if x > 12:       //  implicit
    case x if x > 12:     //  explicit
}    
cxw42 commented 3 years ago

@PureFox48 I much prefer that to overloading as x :+1:

PureFox48 commented 3 years ago

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.

cxw42 commented 3 years ago

@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 .

cxw42 commented 3 years ago

@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:

E.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.

PureFox48 commented 3 years ago

@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.

cxw42 commented 3 years ago

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:

  1. Prioritizing a basic switch without complex conditionals; or
  2. Planning a full-featured switch 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?

mhermier commented 3 years ago

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.

PureFox48 commented 3 years ago

@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.

PureFox48 commented 3 years ago

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:

  1. We allow it anyway.

  2. 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.

mhermier commented 3 years ago

@PureFox48, I think it is a fair point, and that will need proper testing.

cxw42 commented 3 years ago

@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.

cxw42 commented 3 years ago

@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:

  1. <expr>: <stmt>|<block>
  2. case <expr>: <stmts>
  3. (<expr>) <stmt>|<block>

)

PureFox48 commented 3 years ago

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.

cxw42 commented 3 years ago

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.

PureFox48 commented 3 years ago

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 :)

clsource commented 3 years ago

I vote for option 1. I dont like case keyword xd

cxw42 commented 3 years ago

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

ChayimFriedman2 commented 3 years ago

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 šŸ˜ƒ

cxw42 commented 3 years ago

@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!

mhermier commented 3 years ago

@cxw42 don't add DUP, revert its removal 001db02c817c201f1fff4b049a0395ad493061c6

cxw42 commented 3 years ago

@mhermier done, and added some more tests and some complex conditionals.

mhermier commented 3 years ago

@cxw42 Some parts are still not elegants/too much noisy:

mhermier commented 3 years ago

@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.

cxw42 commented 3 years ago

@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?
}
cxw42 commented 3 years ago

@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.