Skip to content
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

Proposal: C-style for loop #8292

Closed
zzyxyzz opened this issue Mar 19, 2021 · 7 comments
Closed

Proposal: C-style for loop #8292

zzyxyzz opened this issue Mar 19, 2021 · 7 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@zzyxyzz
Copy link
Contributor

zzyxyzz commented Mar 19, 2021

This is a proposal to give Zig's while loop a little nudge in the direction of feature parity with C's for loop, the lack of which is a frequently bemoaned (#5070, #8019, #358, #472, #8030).

Doing this would:

  1. provide encapsulation for loop variables, thereby improving readability and safety.
  2. reduce unnecessary annoyance for people coming from C and C++.

Proposed syntax:

loop (var i: i32 = 0; i < limit; i += step) {
    // do something
}

The loop keyword was chosen mostly for its lack of baggage: while would clash with C convention and for with Zig. But this is ultimately just a suggestion and open to alternative proposals.

The loop would support unwrapping just like the current while:

loop (var it = foo.iter(); it; it = it.next()) |val| {
    // do something
} else |err| {
    // do semething else
}

A simplified variant could also be permitted:

loop (cond) {
    // equivalent to current while loop
}

...at which point the while keyword might be dropped entirely.

@jayschwa jayschwa added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Mar 19, 2021
@jayschwa jayschwa added this to the 0.9.0 milestone Mar 19, 2021
@matu3ba
Copy link
Contributor

matu3ba commented Mar 19, 2021

@zzyxyzz

for-loops are used for iterating and while loops for more complex control flow.
Putting more than 3 things into a line also is bad for readability:
1.loop,2.variable stuff, 3.iterator, 4. condition, 5.update, (6. variable name alias = zig special)

Further more one wants to have the initial condition/variable (as precondition of the loop) being explicit (if it is not trivial from the iterator) for simpler code reasoning.

@zzyxyzz
Copy link
Contributor Author

zzyxyzz commented Mar 19, 2021

@matu3ba

for-loops are used for iterating and while loops for more complex control flow.

Ideally, yes. But Zig's highly specialized for loop forces the use of while even for very simple control flow. In Zig it is a very common pattern to 1) declare a loop variable outside the loop, leaking it into the common scope, and 2) use a while loop with the update syntax for what is essentially very straight-forward iteration. This proposal aims to at least prevent the variable leakage (as an alternative to #8019 and explicit scope wrapping).

Putting more than 3 things into a line also is bad for readability:
1.loop,2.variable stuff, 3.iterator, 4. condition, 5.update, (6. variable name alias = zig special)

Sometimes. But note that most of the items you mention are already allowed in Zig, and the resulting long lines are not uncommon in base. This proposal only adds the ability to pull in the loop variable into the same line. This should work fine for most loops, but if the line gets too long, it can be broken up.

Further more one wants to have the initial condition/variable (as precondition of the loop) being explicit (if it is not trivial from the iterator) for simpler code reasoning.

I don't quite get this point, could you clarify?

@pfgithub
Copy link
Contributor

pfgithub commented Mar 20, 2021

loop (var it = foo.iter(); it; it = it.next()) |val| {

If it is of type Iterator that would mean it.next() must return Iterator so it can be stored into it. What type is val and where does it come from?

Did you mean something like:

loop (var it = foo.iter(); it.next(); _ = {}) |val| {
    …
} …

@zzyxyzz
Copy link
Contributor Author

zzyxyzz commented Mar 20, 2021

@pfgithub
That example is indeed pretty nonsensical, thanks for pointing that out.

While trying to find a better one, I ran into a bit of a problem: Apparently the updating and unwrapping variant of while is barely ever used. In all of std I could find only 15 or so instances, mostly involving optional pointers. And of those 15 all but two or three didn't really need to use the update syntax. Here's a typical example taken from linked_list.zig:

pub fn countChildren(node: *const Node) usize {
    var count: usize = 0;
    var it: ?*const Node = node.next;
    while (it) |n| : (it = n.next) {
        count += 1;
    }
    return count;
}

If Zig disallowed combining unwrapping and update clauses that wouldn't make much of a difference:

pub fn countChildren(node: *const Node) usize {
    var count: usize = 0;
    var it: ?*const Node = node.next;
    while (it) |n| {
        count += 1;
        it = n.next;
    }
    return count;
}

And using loop would only impair readability in this case:

pub fn countChildren(node: *const Node) usize {
    var count: usize = 0;
    loop (var it: ?*const Node = node.next; it; it = n.next) |n| {
        count += 1;
    }
    return count;
}

I'm not yet sure how much this affects this proposal. In any case, I should have mentioned that the initializer and updater are supposed to be optional, just like in C. So there's no need for _ = {}. If you don't need the updater, you would write

loop (var it = foo.iter(); it.next() ;) |val| {
     // ...
}

@zzyxyzz
Copy link
Contributor Author

zzyxyzz commented Mar 20, 2021

After thinking about it some more, there are really just two common cases where this proposal might be seen as an improvement over the status quo:

var i: usize = 0;
while (i < len) : (i += 1) {
    ...
}

// -->

loop (var i: usize = 0; i < len; i += 1) {
    ...
}

and

var it = map.iterator();
while (it.next()) |kv| {
    ...
}

// -->

loop (var it = map.iterator(); it.next() ;) |kv| {
    ...
}

There's a tradeoff here between readability and prevention of variable leakage. Which one you prefer is probably a matter of taste. Either way, this proposal no longer seems like a sufficiently large improvement to me, so I'm going to close it for now.

@zzyxyzz zzyxyzz closed this as completed Mar 20, 2021
@Vexu Vexu modified the milestones: 0.9.0, 0.8.0 Mar 22, 2021
@ElectricCoffee
Copy link

The main benefit of the classic for-loop in comparison to the current while loop, is that the iterating variable goes out of scope when the loop is over. Given Zig's lack of shadowing having a variable that automatically goes out of scope is nice.

Yes, you can definitely simulate this by surrounding the while loop with a block, but let's be honest, that's even less readable.

@matu3ba
Copy link
Contributor

matu3ba commented Mar 26, 2021

@ElectricCoffee

I do very much agree that it can happen that the optimiser is not smart enough to elide the variable, which can be an annoying performance loss. (I have no deep understanding of C/C++ semantics, so take this with a grant of salt.)

However, I think that reusing the same variable is a more bad idea for searchability and review.
While loops do some nontrivial stuff in contrast to for loops, so to me the overhead to keep track if a variable is in scope or not is higher.
This also plays nice with forbidding shadowing variables per file scope, since you dont have to worry about other variables with the same name.

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

6 participants