wren-lang / wren

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

Proposal: New loop features #817

Open GlaireDaggers opened 3 years ago

GlaireDaggers commented 3 years ago

Opening an issue to propose adding do, do..while, & continue constructs to Wren. I have a fork which implements these & adds new test cases for them already, but wanted to open a discussion about their syntax & make sure it fits with others' idea of what Wren should be.

The features are of course very simple: a new continue statement which, in any loop (for, while, or do..while), will jump to the iterator or condition check for the next loop iteration. Example:

for(i in 1..10) {
    if(i == 5) continue
    System.print("%(i)")
}
// prints: 1, 2, 3, 4, 6, 7, 8, 9, 10

Can be used to skip the remainder of a particular iteration of a loop early. In the current main branch of Wren this can be done with if statements, but continue would overall reduce the verbosity of such constructs.

The other new feature would be a new loop: do..while. This acts precisely like do..while in most other languages. It's similar to a while, but the evaluation of the condition is deferred until after the loop body.

var i = 0
do {
    System.print("%(i)")
    i = i + 1
} while(i < 10)

This can be handy in cases where the body of the loop sets up the condition to continue the loop, but you need to guarantee that the body is executed at least once first. For example:

var readBytes
do {
    readBytes = readData()
} while(readBytes > 0)

The same scenario could be accomplished in the current main branch of Wren by duplicating the loop body, but do..while would reduce code duplication in that case:

var readBytes = readData()
while(readBytes > 0) {
    readBytes = readData()
}

Additionally, in my fork, the do..while statement can also have its while condition omitted. This creates what is functionally equivalent to a do..while(false):

do {
    System.print("This code is run once")
}

// is functionally equivalent to:

do {
    System.print("This code is run once")
} while(false)

The benefit of this is a block of code which can be broken out of early:

do {
    if(condition) break
    System.print("The rest of this block will be skipped if condition is true")
}

This one seems like it might be less important, but as it was not terribly difficult to add and could still be useful I decided to include it anyway.

mhermier commented 3 years ago

I agree with the 'do { ... } while(...)' addition.

Though, I found the do { ... } block a little bit confusing mostly because you have to reach the end of the block to see if there is or not a while statement, thought I agree it can be a nice complement when editing code live. In addition, when I read it first (before I had to trash my original response), I thought it would be about a forever loop which would have been a great addition, but maybe it is only my expectations.

GlaireDaggers commented 3 years ago

That is an interesting point about having to scan to the end of the block to see its behavior, but I suppose that's already true of a do..while anyway (you have to find the end of the block to see what its continue condition is).

I think it's also a good point about what the behavior of a do block without a while condition should be. The way I originally wrote it, it was a do..while construct without a continue condition - that is, without the continue check, it would simply emit code to "fall through" out of the loop. But should it instead be a loop that, in the absence of a continue condition, simply loops back to the top unconditionally? I figured that having some kind of construct that was a breakable one-off block of code would be useful - while you could certainly emulate that with a do..while(false) or just inserting a break, there's a small possibility that allowing an omitted while statement to become an infinite loop could turn into a footgun of sorts (accidentally writing code that doesn't loop would be in most cases less disastrous than accidentally writing code that loops forever). Would love to get more feedback on this though.

mhermier commented 3 years ago

Well block is already supported. So the question is do we want to have do blocks as an error like other languages, a glorified block to ease live coding, or something else of some meaning, incidentally forever block as some interesting meaning. Every solutions have their pro and cons, and I'm fine with any final decisions.

I don't want to push for the forever loop in that particular form, but because of the simplicity of the compiler, it would also be nice to have that feature in the language somehow, and not waste some time because the compiler can't optimise true/false constant to allow direct branching.

PureFox48 commented 3 years ago

I'd certainly support the addition of continue and do {..} while to the language. Anyone who's programmed in the industry C-family languages (C, C++, C#, Java etc) just expects these constructs to be there and it can be tiresome to program around their absence.

However, I'm not so sure about do {..} for two reasons:

  1. Firstly, I think it's potentially confusing. When I was reading your opening post then, like @mhermier, I was expecting it to represent an infinite loop rather than a once (at most) loop.

  2. I'm not sure that it's important enough to have its own construct as, if we had do {...} while, it could be simulated with do {..} while (false).

As far as an infinite loop is concerned, I think there's a case for adding one which is shorter to type than while (true) though my own preference would be a for loop with an empty range i.e. for() {..}. That would mirror more closely the for (;;) {..} construct which the above mentioned languages often use for such a loop.

avivbeeri commented 3 years ago

I don't think we need do...while but a continue would be highly appreciated, to complement the break statement.

PureFox48 commented 3 years ago

Although I'd personally like to see do...while, I agree that continue is the most important of the three proposals.

mhermier commented 3 years ago

Didn't think of it, but to sum up what new features I would like to see:

GlaireDaggers commented 3 years ago

I think it looks like the safest route to go with a PR is:

PureFox48 commented 3 years ago

Sounds good to me :)

mathewmariani commented 3 years ago

I would love to have a continue to complement break, and do-while loops.

munificent commented 3 years ago

This is up to @ruby0x1, of course, but for what it's worth, I would be happy to see continue and do-while in Wren. I don't think do { ... } for an infinite loop quite carries its weight, especially given the potential confusion.

ruby0x1 commented 3 years ago

I'll have a closer look soon, but the only one I feel inclined toward and have missed countless times over the years is continue.

Wren just isn't the language with all the features, so adding too many ways to do the same thing at the cost of complexity isn't what I see happening.

GlaireDaggers commented 3 years ago

Fair. There's a really good case to be made for trying to keep things simple. While I do think do..while has merit in that I don't simply consider it another way to do loops (its behavior is notably different, and that difference can in several scenarios be quite helpful), I also agree that between those continue is the most important addition.

ruby0x1 commented 3 years ago

The difference between do while and while is that the one executes at least once right? So what I mean is you do have the tools to do that with the language already, it's a specific specialization of while, not a different kind of loop.

i.e these are equivalent, except condition is read once vs twice. The use of break + conditions being set + functions are all valid tools in this context too.

var condition = true
do {
  System.print("once")
  condition = false
} while(condition)
var condition = true
while(condition) {
  System.print("once")
  condition = false
}
mhermier commented 3 years ago

@ruby0x1 these are fundamental blocks, they exist to simplify readability and ease execution flow understanding. They are not that expensive since the loop infrastructure is already in place, and avoid bloated code.

GlaireDaggers commented 3 years ago

@ruby0x1 Yeah, and to be fair I suppose any do..while can be (mostly) emulated as a while with a break condition, albeit with an extra compare. I think I will move ahead with pruning do..while from my branch and just raising a PR for continue. That by itself will already be quite useful.