Proposal: Unify looping to a single keyword
Looping could be simplified into one single "loop" interface/keyword. Which could replace while, for, and foreach. Anything that "loops" uses loop keyword.
While:
int a = 10;
while (a > 0)
{
a--;
}
int a = 10;
loop (a > 0)
{
a--;
}
For:
for (int i = 0; i < 10; i++)
{
io::printfn("%d", i);
}
to
loop (int i = 0; i < 10; i++)
{
io::printfn("%d", i);
}
Foreach:
foreach (&value : values)
{
*value *= 2;
}
to
loop (&value : values)
{
*value *= 2;
}
Loop forever:
for (;;)
{
// ...
}
while (true)
{
// ...
}
to
loop
{
// ...
}
do-while
do {
// ...
} while(condition);
to
loop {
// ...
if condition {
break;
}
}
Some observations:
- Python's base language (OOP aside) is one of the easiest to learn languages and nobody has found it's multiple types of for loops a challenge.
- Go has done as you're suggesting more-or-less, but that has overloaded the
forkeyword so requires a bit more effort and careful reading to see what kind of loop it is. For Go it's pursuit of fewer keywords did not equal a simpler language, because then those keywords become overloaded.
Conceptually, having a single keyword for looping makes a lot of sense. There aren't multiple keyword for other constructs. I think most languages follow tradition in that for and while has been the norm. I like Go and Odin’s approach but I think for was the wrong choice in keyword. Rust added loop but that was after the for and while were already being used so I don't think they took it far enough. I wouldn't say having for/while/foreach is "hard", but I would say having a single "loop" construct is "easier". I do think Go and Odin’s choice made it a simpiler language - one does not have to think "do I need a while loop, for loop, or for each loop", I just need a loop. Just my two cents, obviously comes down to preference.
The advantage of a keyword is that scanning the left column I can instantly see what kind of loop it is, otherwise I have to read more text. It then tells me how to interpret the expression that follows, for instance:
foreach (&value : values)
{
*value *= 2;
}
This seems like a reduction in clarity, because it looks quite close to the for loop equivalent, the main difference being : instead of ; and having one fewer ;
loop (&value : values)
{
*value *= 2;
}
Why would one care what type of loop it is? The only reason to care is if one cares about the looping logic and if that is the case, one would have to look at the expression either way. A single keyword is more intuitive for scanning and searching than if it is "one of three keywords".
Our brains work by one-shoting the structure of code, much like how we understand a picture (we don't read a picture left to right etc.). So in reality we can just look at statement without reading and know what type of loop it is based on the structure - ;; vs : vs condition.
An evolution from C is unlikely to drop its looping constructs.
More damning, in addition to this there are ambiguities in conflating them, especially foreach.
Let's look at foreach (&bar : baz). The composition of a for-loop is for ( <EXPR_LIST>; <EXPR_LIST>; <EXPR_LIST> ) <STMT>
If we would try to use the following syntax: for (&bar : baz) then what happens is that we start to parse &bar as "address of bar", then when we see : we need to rollback and reinterpret the entire statement as a foreach. This complicates not just parsing, but also code reading. This is why foreach and for isn't unified in the first place. The for (int x : some_expr) of say Java is different, because it can always read the first expression, and then determine foreach/for from the presence of ":" if the expression is a declaration. This is in comparison to C3 which has variants like foreach (int i, int b : baz), foreach (i, &b : baz) etc. It also doesn't take into account reverse iteration (although that's more of a nitpick)
Next we have the while statement, which is uniquely well shaped for optional result iteration: while (try x = foo()) { ... }. for cannot emulate that unwrap in a good way.
Finally, while do-while is rarely used, it's expressing something neither for nor while does.
I have just one question "Why?"
C3 is supposed to be an evolution of C and is being "sold" as one, you want C programmers to feel mostly comfortable, no? I think changing everything to loop is a bad idea. Also consider the fact it'll end up breaking everything, even if introduced gradually.
This also gives me vibes you are trying to bring Rust into the language? It has loop {} you mentioned.
Sorry if this comes off as an attack, not my intention.
If we would try to use the following syntax:
for (&bar : baz)then what happens is that we start to parse&baras "address of bar", then when we see:we need to rollback and reinterpret the entire statement as a foreach. This complicates not just parsing, but also code reading. This is whyforeachandforisn't unified in the first place. Thefor (int x : some_expr)of say Java is different, because it can always read the first expression, and then determine foreach/for from the presence of ":" if the expression is a declaration. This is in comparison to C3 which has variants likeforeach (int i, int b : baz),foreach (i, &b : baz)etc. It also doesn't take into account reverse iteration (although that's more of a nitpick)
I will say I am not deeply familiar with the parser implementation. But it looks like a recursive decent and I know one could probably parse everything in loop (...) as a one pass fairly trivially (or with only a few token lookaheads), or one could try and rollback as you stated. I do imagine in practice this would be negligible even on extremely large projects.
As stated earlier I don't believe the code readability argument in practice. But I understand how some may think that way.
Next we have the
whilestatement, which is uniquely well shaped for optional result iteration:while (try x = foo()) { ... }.forcannot emulate that unwrap in a good way.
loop (try x = foo()) { ... } also makes sense to me. While I agree while is better than for here.
Finally, while do-while is rarely used, it's expressing something neither
fornorwhiledoes.
I think the way loop models this is actually more intuitive
loop {
// ...
if condition {
break;
}
}
I have just one question "Why?" C3 is supposed to be an evolution of C and is being "sold" as one, you want C programmers to feel mostly comfortable, no? I think changing everything to
loopis a bad idea.
I'd agree if I thought C3 was trying to be a superset of C. But I think things have diverged enough that syntactically (not functionally) that it looks just as different/same as most other C family languages. So I don't know if just following tradition here is enough on its own. It's simple enough to tell someone - "If you want a loop use loop". So I don't think new developers will have much confusion. Then things just work - "Loop on condition?" Easy, "Loop by index" as you expect, "Loop through elements" we got you covered.
As I said in my first response, I think it really just comes down to preference. I know in Rust a lot of people hated the idea of the postfix await syntax (variable.await rather than await variable). Since historically no language had went this route. But almost everyone today agrees it was a good idea after using it.
I think Go took a step in the right direction. And having a single loop keyword makes a lot of sense, four for/while/foreach/do seem excessive.
This also gives me vibes you are trying to bring Rust into the language? It has loop {} you mentioned.
I think there are things a lot of languages do well and not so well. We should always try to take inspiration where things work if it fits the domain. I do like the loop keyword. I didn't when I first started Rust funny enough. I thought "this is dumb, we already can do while(true)". Maybe even Go would have used loop instead of for if it started after Rust and took inspiration from it.
Sorry if this comes off as an attack, not my intention.
None taken as so. I hope mine does not as well. Discussing ideas here not individuals merits. But I appreciate the comment.
Last I'll say unless someone wants a reply to a specific point. Ultimately up to the maintainers and community on what is decided.
I will say I am not deeply familiar with the parser implementation. But it looks like a recursive decent and I know one could probably parse everything in loop (...) as a one pass fairly trivially (or with only a few token lookaheads), or one could try and rollback as you stated. I do imagine in practice this would be negligible even on extremely large projects.
It is not a matter of parsing cost, but that of complexity. This complexity means the grammar would no longer be LL(1) unless it's written VERY loosely, which is a problem in itself. It this heaps complexity onto any tool wanting to parse C3 correctly.
And more importantly, I've found that necessary lookahead for the compiler usually correlates to lookahead for the programmer, meaning it's in practice less easy to read quickly.
Regarding "it diverged enough from C to warrant considering further divergence". That's not really how the decision is made. From the website: "Stay close to C - only change where there is a significant need."
There are many possible changes and I do personally like name->type declarations, trailing return types and many other things. However, those are at best marginal improvements over status quo, so they are not considered.
Replacing for/while/do with a unified loop construct, only for the goal of unifying loops is likewise at best a marginal improvement. I'm aware of loop of course. It's definitely a nicer way to express an infinite loop than for (;;) or while (true). I am far from convinced it's a good way to express all of for/while/foreach/do functionality (and how do you even express "do" in a good way with loop even?). On top of this we have the grammar issues with such a solution.
Having multiple looping constructs is not minimal, but that is not the goal – it's to be familiar and easy to use, and I don't see how overloading a loop construct with the functionality of 4 different constructs achieves this?
Rust has its own semantics and constructs, just because it works in Rust doesn't mean it will be a good fit in an evolution of C. If we look at C-derived languages, such as C#, Java, and even more distant ones like PHP, they all have chosen to retain all of these statements. It would be more of a revolution to remove them and replace them with loop.
Is there something further to add to this discussion @mcmah309, or should I close it?
I'd still like to see it. Nothing else to add unless you were looking for a response from me to your last. Since ultimately it is a preference thing, I don't see minds being changed here from further discussion. If the decision is made to keep looping as is, then sure closing is fine by me.
i think it's bad design...