Why doesn’t Zig have C-style `for` loops?

I usually use Zig’s for loops for most things, but sometimes I need to use traditional C-style loops with an initialization step. Here’s an example from something I’m working on:

{var i: u8 = 0; while (true) : (i += 1) {
    // ...
}}

I don’t want to increase the indentation level just for one variable, so I usually resort to the above. I think it looks ugly. I think that the current for should be renamed to foreach, and the new for should be like C’s for loops:

for (var i: u8 = 0) : (true) : (i += 1) {
    //...
}

What do you think?

Been suggested many times and rejected each time.

Potentially relevant:

Adding a new feature just to save a few lines of code isn’t really what Zig is interested in in my experience. Note also that for loops that work on ranges (e.g. for (0..end_index) |i| {) only got added as a side-effect of a different change with other use cases in mind: Proposal: Multi-object `for` loops · Issue #7257 · ziglang/zig · GitHub

6 Likes

I have thought about this many times, the C style for loops is definitely one of the things I miss in Zig. However every time I think about it, I notice that a hypothetical Zig version is just too verbose and doesn’t match the simplicity of the C-style for loop:

for(var i: i32 = 0; i < len; i += 1)
for(int i = 0; i < len; i++)

This is even more true for your proposal, which adds colons everywhere and requires even more upfront knowledge.

I personally think that a syntax involving captures (similar to the current for) would fit best in the language, since that would make the index immutable by default. But so far I haven’t found anything good enough for a proposal.

I think many do, especially when nesting, and frequently end up not doing it, and instead reuse variables in the same scope. There are many examples in the wild, which I think this is pretty brittle in particular when refactoring.

Seems to be somewhat common

3 Likes

It’s not about saving lines of code, it’s about preventing the index variable from leaking to the outer scope. The fact that the only way to do this in current Zig is ugly means that a lot of people just won’t do it.

2 Likes

True, but what I proposed would be more flexible. For example:

for (var start: usize = 0)
 : (std.mem.indexOfScalarPos(u8, str, start, '\n')) |idx| {
    //...
}

Also, I wouldn’t mind copying C for loops with the single pair of parentheses and two semicolons but I copied Zig syntax for familiarity.

Which you can do by adding a few lines of code.

From the comment that closed one of the linked proposals:

Use anonymous blocks to limit the scope of variables.


(slightly interestingly, this aspect of Zig has gotten me doing the same thing in other programming languages, e.g. when I’m writing Lua now I find myself writing

do
  -- stuff
end

somewhat often, which I never really did before Zig)

1 Like

This is more more verbose and significantly harms readability. As mentioned by @cryptocode, many choose to not do it. You can get around the problem sure, but that doesn’t mean the problem doesn’t exist.

Is the inner index variable always the only one which is specific to a while loop? No, right? So with anonymous blocks, all of those gets scoped. With for statements, it’s easier to just set them up before the loop, then they leak when that loop is over.

I don’t think it’s ugly at all, and have always found this kind of argument to be weak advocacy for a position.

I like that the language is made of fully fleshed out primitives which compose well together. Sugar is bad for your liver.

1 Like

True, but in my experience this doesn’t move the needle on proposals.

This is debatable, but as you can see by the reactions to Andrew’s comments in a place to put variables in only while loop scope · Issue #5070 · ziglang/zig · GitHub, many people agree with you. However, ‘many people want/think X’ also doesn’t move the needle on proposals (for example, many of the proposals with the most ‘thumbs up’ reactions have been rejected).

A relevant blog post:

4 Likes

I would rather have something like this:

for (u8.{0..16}) |i| {
    // ...
} 

Where u8.{0..16} defines a special span type that for() knows how to make use of. The advantage here is that i would be const not var, eliminating potential misunderstanding due to unexpected modification of the index within the loop.

The theoretical construct would allow you to loop through multiple spans:

for (u8.{ 0..16, 48..50, 78..128 }) |i| {
    // ...
} 
3 Likes

Just use a range.
for the second example an array of start and end of the ranges
I know is always usize, but you can @intCast. Ideally you would be able to specify the type of the range, but that was explicitly denied

IMO, embedding initialization in loops leads to bugs esp w/multiple loops and memory allocation…you have an init, need a de-init too! zig gets it right 100%.

for (var outer = alloc_outer_struct() ... )  {
  for (var inner = alloc_inner_struct()  ... )  { //ahh...C...ty for this gift.
    ...
  }
}
1 Like

We could solve this by changing how zig fmt formats blocks containing just a var and a loop.

Before:

{
    var i: u8 = 0;
    while (true) : (i += 1) {
        // ...
    }
}

After:

{ var i: u8 = 0; while (true) : (i += 1) {
    // ...
} }

:upside_down_face:

3 Likes

Yes

There’s a non-trivial chance that we’ll get that with integer ranges. There’s been active interest from the compiler team in implementing ranges, I consider it one of the most high-impact additions to the language which are on the table.

4 Likes