Skip to content

Proposal: Use a keyword instead of a colon in while loop continue expressions #8030

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

Closed
Rocknest opened this issue Feb 18, 2021 · 31 comments
Closed
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@Rocknest
Copy link
Contributor

In status quo zig continue expressions are not enough visually separated from the while condition, you have to parse it mentally a bit longer to know is it a long condition expression or its two expressions.

Also i think it would be easier to new users to understand what that expression does mean.

How it would look like:

const expect = @import("std").testing.expect;

test "while loop continue expression" {
    var i: usize = 0;
    while (i < 10) continue (i += 1) {}
    expect(i == 10);
}

test "while loop continue expression, more complicated" {
    var i: usize = 1;
    var j: usize = 1;
    while (i * j < 2000) continue ({ i *= 2; j *= 3; }) {
        const my_ij = i * j;
        expect(my_ij < 2000);
    }
}
@Rocknest Rocknest changed the title Proposal: Use a keyword instead of a semicolon in while loop continue expressions Proposal: Use a keyword instead of a colon in while loop continue expressions Feb 18, 2021
@Rocknest
Copy link
Contributor Author

@g-w1 Yes, thank you. May i ask you to explain why did you downvote?

@g-w1
Copy link
Contributor

g-w1 commented Feb 18, 2021

I think it introduces too much complexity. Especially the part where you can use blocks { } in it. This could be (and probably will) abused to make the code even less readable. Also the syntax is ambitious since it can be parsed as while (i < 10) { continue (i += 1) {} } which is how my eye parsed it.

@Rocknest
Copy link
Contributor Author

@g-w1 you already can use blocks in it. I took these examples from the langref.

@g-w1
Copy link
Contributor

g-w1 commented Feb 18, 2021

Oh sorry about that, I guess I didn't read carefully enough, but my second point still stands.

@raulgrell
Copy link
Contributor

I only really dislike the length of the keyword continue, but I can't find any other arguments against this. In fact, it's consistent with Zig's principle that control flow is described by keywords, and this is definitely an example of control flow.

If we were willing to add another keyword, I'd propose as, but this clashes with the @as(type) builtin.

Perhaps it would be enough to use a more visually distinct symbol? A new :: token? Reuse =>?

@batiati
Copy link
Contributor

batiati commented Feb 19, 2021

Hi Folks, my 20 cents

I think that continue should be used only to jump back to the beginning of the loop.
why not use the classic for syntax?

while (i < 10; i += 1) {}

@raulgrell
Copy link
Contributor

@batiati This has come up before many times in an attempt to address various issues. In fact, that exact syntax was originally used in Zig. I address it somewhat in #472

There are some proposals currently open like #8019 and some that are closed like #5070, #358, #357.

I also just realised the continue: syntax was proposed before:
#357 (comment)

@batiati
Copy link
Contributor

batiati commented Feb 21, 2021

That use case is a kind of thing that makes me a big fan of zig, so, lets forget about the classic for sintax 😁

while(maybe) | value | : ( maybe = value.next() ) { } 

@windows-h8r
Copy link

Why don't we remove this syntax altogether in favor of defer statements?

@Rocknest
Copy link
Contributor Author

@windows-h8r its not a good idea, there would more noise in the loop body and i like seeing at a glance what the loop will do when it repeats.

@raulgrell
Copy link
Contributor

@windows-h8r defer statements aren't able to get us the required semantics. See #472 (comment)

@windows-h8r
Copy link

@windows-h8r its not a good idea, there would more noise in the loop body and i like seeing at a glance what the loop will do when it repeats.

On the contrary, the current syntax encourages writing long while loop headers.

@RogierBrussee
Copy link

RogierBrussee commented Feb 22, 2021

So the main problem seems to be to come up with a good short word. Maybe

"step"

fits the bill?

while (i < 10) step (i += 1){
...
}

while (item)|it| step (item = it.next){
....
}

This needs a new keyword "step", but "continue" is not overloaded with a new meaning.

@windows-h8r
Copy link

windows-h8r commented Feb 22, 2021

What about an infix defer operator with the same semantics?

var i: usize = 0;

while (i < 10 defer i += 1) {
    std.debug.print("{}\n", .{i});
}

@Matthew-Amos
Copy link

I'll play devil's advocate here, why have this feature at all?

const expect = @import("std").testing.expect;

test "while loop, with all the bells and whistles" {
    var i: usize = 1;
    var j: usize = 1;
    while (i * j < 2000) : ({ i *= 2; j *= 3; }) {
        const my_ij = i * j;
        expect(my_ij < 2000);
    }
}

test "while loop, but boring" {
    var i: usize = 1;
    var j: usize = 1;
    while (i * j < 2000) {
        const my_ij = i * j;
        expect(my_ij < 2000);
        i *= 2;
        j *= 3;
    }
}

Maybe it's just me, but you can replace : with anything and that first example is still going to take me longer to process.

@batiati
Copy link
Contributor

batiati commented Feb 25, 2021

I'll play devil's advocate here, why have this feature at all?
Maybe it's just me, but you can replace : with anything and that first example is still going to take me longer to process.

You can use the continue like that:

const expect = @import("std").testing.expect;

test "while loop, with step" {
    var i: usize = 1;
    var j: usize = 1;
    while (i * j < 2000) : ({ i *= 2; j *= 3; }) {
        const my_ij = i * j;

        //I want just odd numbers
        if (@rem(my_ij, 2) == 0) continue;
        expect(my_ij < 2000);
    }
}

test "while loop, without step" {
    var i: usize = 1;
    var j: usize = 1;
    while (i * j < 2000) {
        const my_ij = i * j;

        //I want just odd numbers
        if (@rem(my_ij, 2) == 0) continue;
        expect(my_ij < 2000);

        //this "step" block will not run on continue!
        i *= 2;
        j *= 3;
    }
}

@mlawren
Copy link

mlawren commented Mar 8, 2021

Here is an alternative suggestion using then at the end:

const expect = @import("std").testing.expect;

test "while loop continue expression" {
    var i: usize = 0;
    while (i < 10) {} then {i += 1};
    expect(i == 10);
}

test "while loop continue expression, more complicated" {
    var i: usize = 1;
    var j: usize = 1;
    while (i * j < 2000) {
        const my_ij = i * j;
        expect(my_ij < 2000);
    } then {
        i *= 2;
        j *= 3;
    }
}

while can already have an else branch, so this feels like it would fit inbetween fairly naturally.

@ElectricCoffee
Copy link

ElectricCoffee commented Mar 14, 2021

Allow me to come up with a counter-proposal, which is similar to @mlawren's then proposal;

while (condition) {
    // do stuff
} continue {
    // do stuff at the end of iteration or when `continue` is called.
}

This would extend beyond while loops and into for loops as well, where you'd be able to do

for (collection) |item| {
    // do stuff
} continue {
    // special continuation logic
}

This wouldn't interfere with the else on looping expressions, as you'd just be able to put that at the end, potentially creating something a-la this for shorter loops

var foo = 
    while (stuff) { ... }
    continue { ... }
    else { ... };

The reason I prefer the continue keyword to be reused here, is that it makes it clear that this is related to the internal continue, in that it would be called either at the end of an iteration or when continue is naturally called.

then makes it feel like it's something called after the loop is done iterating, akin to finally in Java's exceptions.

Alternatively, it could be called next, in case the continue keyword could be ambiguous in the context of nested loops

@SpexGuy SpexGuy added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Mar 16, 2021
@windows-h8r
Copy link

@ElectricCoffee
But then there's no difference between

while (condition) {
    statement1;
} continue {
    statement2;
}

and

while (condition) {
    statement1;
    // continue
    statement2;
}

@Rocknest
Copy link
Contributor Author

@windows-h8r Incorrect.

while (cond) {
    // cond2=true
    if (cond2) continue;
    statement1; // skip
} continue {
    statement2; // execute
}

while (cond) {
    // cond2=true
    if (cond2) continue;
    statement1; // skip
    statement2; // skip
}

@ElectricCoffee
Copy link

@ElectricCoffee
But then there's no difference between

while (condition) {
    statement1;
} continue {
    statement2;
}

and

while (condition) {
    statement1;
    // continue
    statement2;
}

You're right, in the case when there's no continue statement.
But you can say the same about the existing syntax:

while (condition) : (i += 1) {
     stuff();
}
// is the same as
while (condition) {
    stuff();
    // :
    i += 1;
}

The point here is to have a syntax that is executed at every iteration regardless of circumstance.

Using a continue block just lets you have more expressions without it looking cluttered.

The syntax is ripped wholesale from Perl [src], except in Perl the block is called continue and the keyword is called next, which is partly why I suggested doing the inverse.

@windows-h8r
Copy link

@windows-h8r Incorrect.

while (cond) {
    // cond2=true
    if (cond2) continue;
    statement1; // skip
} continue {
    statement2; // execute
}

while (cond) {
    // cond2=true
    if (cond2) continue;
    statement1; // skip
    statement2; // skip
}

I stand corrected. However, that's such a subtle difference, despite the fact that they both use the continue keyword.

Moreover, in my humble opinion, a language that uses a keyword as a statement in some contexts and a clause in other contexts is not a well-designed language.

@ElectricCoffee
Copy link

@windows-h8r Incorrect.

while (cond) {
    // cond2=true
    if (cond2) continue;
    statement1; // skip
} continue {
    statement2; // execute
}

while (cond) {
    // cond2=true
    if (cond2) continue;
    statement1; // skip
    statement2; // skip
}

I stand corrected. However, that's such a subtle difference, despite the fact that they both use the continue keyword.

Moreover, in my humble opinion, a language that uses a keyword as a statement in some contexts and a clause in other contexts is not a well-designed language.

This is why I proposed this alternative syntax as well:

while (cond) {
    // cond2=true
    if (cond2) continue;
    statement1; // skip
} next {
    statement2; // execute
}

@windows-h8r
Copy link

windows-h8r commented Mar 18, 2021

@ElectricCoffee That solves the second problem, but exacerbates the first one: The semantics aren't obvious.

I have another idea. Remove the : syntax, as well as the errdefer statement, and combine their functionalities by parameterizing defer.

defer(.All) array.deinit();
defer(.Loop) i += 1;
defer(.ReturnErr) a.b.deinit();

@ElectricCoffee
Copy link

ElectricCoffee commented Mar 18, 2021

@ElectricCoffee That solves the second problem, but exacerbates the first one: The semantics aren't obvious.

I have another idea. Remove the : syntax, as well as the errdefer statement, and combine their functionalities by parameterizing defer.

defer(.All) array.deinit();
defer(.Loop) i += 1;
defer(.ReturnErr) a.b.deinit();

@windows-h8r the semantics would be the same as : except it would allow for multiple statements. If the semantics aren't obvious, then it's because they weren't obvious originally.

The main issue I see with your defer idea, is that you can put it anywhere in the loop.

I see this as bad, because it makes it hard to spot at a glance, especially if your loop is complex or messy.
Relegating the behaviour to a dedicated block at the end of the loop makes it easy to find.

Remember: programming languages only exists for humans to read; if they didn't we'd all be writing in binary.
The more obvious and clear something is to a human reader, the better.

And yes, obvious semantics definitely help with that, so now it's a toss-up between obvious semantics and obvious obvious placement.

How does defer work in a loop?

EDIT: include the version of the comment I actually replied to

@windows-h8r
Copy link

@ElectricCoffee Good point.

The step keyword was previously suggested. I'm in favor of that.

while (i < 10) step (i += 1) {
    sum += nums[i];
}

@ElectricCoffee
Copy link

@windows-h8r How is that any better than the block though? Except for limiting what code you can put in it

@windows-h8r
Copy link

windows-h8r commented Mar 18, 2021

@windows-h8r How is that any better than the block though? Except for limiting what code you can put in it

  • The code goes at the beginning of a loop instead of the end
  • step is a better name than next
  • The semantics are more clear
  • It's only a slight tweak in the grammar

@ElectricCoffee
Copy link

The code goes at the beginning of a loop instead of the end

The code is executed at the end though

step is a better name than next

step makes it sound like it's a step-size, next means "next iteration"

The semantics are more clear

The semantics are identical

It's only a slight tweak in the grammar

That's fair

@windows-h8r
Copy link

windows-h8r commented Mar 18, 2021

@ElectricCoffee

The code is executed at the end though

I know. That's the point.

step makes it sound like it's a step-size, next means "next iteration"

On the contrary, the keyword next is the equivalent of continue in Ruby and Perl, and ALGOL 58 uses the step keyword like this:

for i := 0 step 0.5 until 10 do
begin
    print(i);
end;

The semantics are identical

No. The for(start; stop; step) syntax should be familiar to almost any programmer, and the keyword step is even more clear than that. However, if I saw this code fragment for the first time, I would have gone insane:

var foo = while (stuff) {
    ...
} next { 
    ...
} else {
    ...
};

The semantics of an else clause on a while loop are not obvious, let alone a next clause.

@ElectricCoffee
Copy link

ElectricCoffee commented Mar 18, 2021

The semantics for an else clause are particularly problematic. Is it executed when a loop ends, or when the condition fails the first time a la if (cond) { while (cond) { ... } } else { ... }.

Your argument about putting a step in the while loop is exactly WHY we need a C-style for loop instead.

The block I'm proposing lets you do far more complex things than just setting a step size (and yes, that is what the ALGOL code does, it sets a step size of 0.5). Yes, it looks ridiculous for just a single expression, but the point is for it to allow for much more than that for example:

while (current_word != EOF) {
  // do stuff to process the word as part of some parsing
} next {
  // this happens no matter what at the end of every iteration
  current_word = advance_string(&str);
  current_word = trim(to_lower(&current_word));
}

There's no way to do that nicely with the proposed step syntax, while it flows naturally here.

@ghost ghost mentioned this issue Mar 19, 2021
@andrewrk andrewrk added this to the 0.8.0 milestone May 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests