Zig: a place to put variables in only while loop scope

Created on 16 Apr 2020  Â·  20Comments  Â·  Source: ziglang/zig

   {
        var i: usize = 0;
        while (i < 10) : (i += 1) {
            list.append(@intCast(i32, i + 1)) catch @panic("");
        }
    }

    {
        var i: usize = 0;
        while (i < 10) : (i += 1) {
            testing.expectEqual(@intCast(i32, i + 1), list.inner.items[i]);
        }
    }

edit: this is the pattern that @andrewrk chose to settle with

this is from @Hejsil 's interface demo. also see a reddit thread that mentions this pattern

doing this is to avoid:

   var i: usize = 0;
   while (i < 10) : (i += 1) {
       list.append(@intCast(i32, i + 1)) catch @panic("");
   }
    var i_2: usize = 0;
    while (i_2 < 10) : (i_2 += 1) {
       testing.expectEqual(@intCast(i32, i_2 + 1), list.inner.items[i_2]);
    }

compare with a C for loop:

for (int i = 0; i < 10; i++) {}
assert(i == 10);

the c for loop has

  • one place to init loop state
  • one place to put terminating condition
  • two places to write how the state updates each loop ¯_(ツ)_/¯

versus an example from the zig docs:

const assert = @import("std").debug.assert;

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

zig while loop has:

  • nowhere to init state native to the loop ¯_(ツ)_/¯
  • one place to put terminating condition
  • two places to write how the state updates each loop ¯_(ツ)_/¯

related zen:
_avoid local maximums_
_communicate intent precisely_
_favor reading code over writing code_
_only one obvious way to do things_
_reduce the amount one must remember_
_minimize energy spent on coding style_

status quo inhibits all of these. can we make this better?

proposal

Most helpful comment

You can also do it at runtime and without reserving static memory like this:

const std = @import("std");

fn rangeHack(len: usize) []void {
    // No chance of UB [*]void is a zero-size type
    return @as([*]void, undefined)[0..len];
}

pub fn main() void {
    for (rangeHack(10)) |_, i| {
        std.debug.warn("{} ", .{i});
    }
}

Output:
0 1 2 3 4 5 6 7 8 9

But this is definitely a hack.

All 20 comments

Idea: currently, we support this:

while (foo) : (bar) {
    baz
}

Should we also support this?

while (init) : (foo) : (bar) {
    baz
}

?

Or, alternately,

(init) : while (foo) : (bar) {
    baz
}

e.g.

(var a: usize = 0) : while (a < 8) : (a += 1) {

}

A refined variation of the latter might be good, as it has the following properties:

  • Easier to read
  • Init is clearly distinct from condition and state-update
  • Variable is clearly scoped

interesting ideas, but

(var a: usize = 0) : while (a < 8) : (a += 1) {}

your proposal has:

  • one place to init loop state
  • one place to put terminating condition
  • two places (bar and baz) to write how the state updates each loop ¯_(ツ)_/¯

so its just like a c for loop

Anyone coming from another C like language is instantly going to know how to write and read loop declarations like C:

for (initialization ; condition; increment) {
    body
}

Maybe I'm missing something. Is there a reason to avoid using C style loops?

I assume the purpose of this is to figure out how to declare new variables in the scope of the for loop. If this is the case, JavaScript does something similar:

for (var i = 0; i < 100; i++) {
    console.log(i);
}

It becomes worse when we add more variables... :wink: I think starting a new block is a good solution in this condition. Otherwise just give variable names i, i2, i3, i4, etc.

two places to write how the state updates each loop ¯\_(ツ)_/¯

Problem solved if the variables defined in (...) cannot be modified in the loop body {...}. Or use a Range:

for range(1, 10) do_something_ten_times();

// let's specify the type in |...| if the compiler cannot figure it out
for range(begin, end) |value: i8| {
    // first, break if (value + 1 > end)
    ...
    // finally, break if (value + 1 > end)
}

// `count` starts from 0
for range(begin, end) |value: i8, count: u8| {
    ...
}

// `step` can be zero; `for range` does not throw error
for range(begin, end, step) |value: i8| {
    ...  // `step` is hidden and unmodifiable; we use its absolute value
    // if (step > 0) break if (value + step > end)
    // if (step < 0) break if (value - abs(step) < end)
}

here is the rest of my proposal:

// rename `for` to `foreach`
for each(elements) |value| { ...  }
for each(elements) |value, index| { ...  }

// optionally add a counter which starts from 0
while (success) |content, count: usize| {
    ...
}

edit3: only use the absolute value of step in case numbers are unsigned

↑ updated: changed range(min, max) to range(begin, end, non_zero_step)

edit: well, if you really want zeros that's no problem too

An attempt at finding symmetry between if and while

No, you should take for range as one keyword just like forrange (-rr- is weird). Although we can also write for (@range(x, y, step)) |v, i| but I prefer foreach, then foreach (@range(x, y, step)) |v, i| is a bit verbose. Anyway I can accept both for range/each and the original for syntax.

@range() cannot be used with while because it doesn't return bool, optional nor error.

Right now Zig's for loops already act like a foreach. It is possible to implement ranges in compile-time Zig doing something like this:

fn range(comptime T: type, comptime start: usize, comptime end: usize) []T {
    const Len = end - start;
    comptime {
        var result: [Len]T = [_]T{0} ** Len;
        var i: usize = 0;
        while (i < Len) : (i += 1) {
            result[i] = start + i;
        }

        return result[0..];
    }
}

test "range" {
    for (range(i64, 0, 10)) |i| {
        std.debug.warn("{} ", .{i});
    }
}

Output:
0 1 2 3 4 5 6 7 8 9

Caveat this will not work for runtime only array lengths however for already gives you this:

for (runtimeArray) |value, index| {

}

You can also do it at runtime and without reserving static memory like this:

const std = @import("std");

fn rangeHack(len: usize) []void {
    // No chance of UB [*]void is a zero-size type
    return @as([*]void, undefined)[0..len];
}

pub fn main() void {
    for (rangeHack(10)) |_, i| {
        std.debug.warn("{} ", .{i});
    }
}

Output:
0 1 2 3 4 5 6 7 8 9

But this is definitely a hack.

Anyone coming from another C like language is instantly going to know how to write and read loop declarations like C:

for (initialization ; condition; increment) {
    body
}

Maybe I'm missing something. Is there a reason to avoid using C style loops?

I assume the purpose of this is to figure out how to declare new variables in the scope of the for loop. If this is the case, JavaScript does something similar:

for (var i = 0; i < 100; i++) {
    console.log(i);
}

+1 for this question. What is the aversion to C-style for loops? I am okay with a for-range syntax, but I find the C-style for loop much easier to type and easier to read than the current while loop idiom and unfortunately I find myself using that idiom more than for loops as they are.

my only issue with C-style for loops is they have two different places to write the loop body

for (int i = 0; i < 10; i++) {}
// versus
for (int i = 0; i < 10;) {i++;}

isnt this redundant? zig has the same issue

var i : usize = 0;
while (i < 10) {i += 1;}
// versus
var i : usize = 0;
while (i < 10) : (i += 1) {}

my only issue with C-style for loops is they have two different places to write the loop body

for (int i = 0; i < 10; i++) {}
// versus
for (int i = 0; i < 10;) {i++;}

isnt this redundant? zig has the same issue

var i : usize = 0;
while (i < 10) {i += 1;}
// versus
var i : usize = 0;
while (i < 10) : (i += 1) {}

for (var i: usize = 0; i < 10; i+=1) is less characters and I find it more natural to write than
var i: usize = 0; while (i < 10) : (i+=1) {. And plus the i is automatically scoped unlike the while idiom (which would require a {} block)!

Alternatively we could have something like for (0..10) |i| { or for (0..10:2) |i| { (where 2 is the step) which is better than C-style for loops. By my main point is that if we don't want to do for-range loops, even C-style for loops are strictly better than the while idiom we have now, unless I am missing something.

isnt this redundant? zig has the same issue

They are not redundant. These three loops have slightly different semantics in Zig:

var curr: ?*Node = getListHead();

// loop 1
while (curr) |node| : (curr = node.next()) {
    if (cond_a) { continue; }
    if (cond_b) { break; }
}

// loop 2
while (curr) |node| {
    defer curr = node.next();
    if (cond_a) { continue; }
    if (cond_b) { break; }
}

// loop 3
while (curr) |node| {
    if (cond_a) { continue; }
    if (cond_b) { break; }
    curr = node.next();
}

Loop 1: curr = node.next() will execute on continue but not on break
Loop 2: curr = node.next() will execute on both continue and break
Loop 3: curr = node.next() will not execute on continue or break

Use anonymous blocks to limit the scope of variables.

nymous blocks also work well - the label is a comment and the first curly is pushed over where it belongs.

findOrCreateKey: {
   var i = 0;
   while (...) : (i += 1) {
       ...
   }
}

I realize this issue is closed but after reading some of the comments I see an idea that could address this and other issues. Namely, to provide a way to define symbols for the next statement/expression. Something like this:

with (vars...) <block/statement>

So for loops you'd have:

with (var i : usize = 0) while (i < 10) : (i += 10) {
    ...
}

And this could also be used to define symbols/aliases for functions:

with (comptime const T = @TypeOf(a).Pointer.child)
pub fn foo(a: var) !T {
    var b: T = undefined;
    // ...
    return T.init(...);
}

Maybe you could use struct literals for the with <struct_literal> but not sure.

I'm not so much a fan of anonymous blocks when you have a lot of nested loops

Desired:

while(std.utils.Range(0,10)) |i| { // statement cannot both be a declaration and a '.next()' call
   // do stuff
} 

Creating an iterator is not the same as a testing a boolean expression though.

Maybe the for syntax could be changed to expect iterator declarations (with next() / hasNext() "methods") instead of expecting slices, and while could stick to accepting a condition expression returning a bool or an optional.

for(std.utils.SliceIterator(arr[4..10]) |value| {
     // do stuff
}
const iterate = std.utils.SliceIterator;
for( iterate(arr[4..10])) |value| {
     // do stuff
}

for(std.utils.Range(0,10) ) |i| {
     // do stuff
}

Uglier, but more versatile?

There is one thing that is open to changing here which is having zig fmt support this form:

{var i: i32 = 0; while (i < 10) : (i += 1) {
    blah();
}}

At that point, it really looks like a C for loop with a bit more characters. Which both means people coming from C like languages will both feel more at home with that syntax form, and leave them wondering why the syntax just does not mimic C.

{var i: i32 = 0; while (i < 10) : (i += 1) {
    blah();
}}

inconsistent coding style on the spot, nitpicking starts...

While if/while/for/switch all support |capture|, the later three all have their specialty:

  • while: : (i += 1) where i += 1 is not an expression. for(int i=0;i<10;i+=1) must cry
  • for: only works on a slice, without too much hidden logic in C++'s for(auto x : container)
  • switch: case const_min ... const_max_inclusive where ... cannot be used elsewhere and .. also not supported

well, just small complaint. I think for (0..10) |i| is considerable for the use case above. And in case anyone may think this is slippery slope theory, let's drop support for for (slice) |v, i|. This way if/while/for/switch all capture one value, in the meantime also leave some design space for the tuple syntax and structural assignment.

// `i` is optional
for (arr[0..10]) |&v, i| { v.* = i; }
// becomes
for (0..10) |i| { arr[i] = i; }
// now optional `i` is gone

if (opt_tuple) |x| f(x[0]);
// must capture all elements?
if (opt_tuple) |x, y| g(x, y);

// do not need to capture all fields?
if (opt_struct) |.x, .y| g(x, y);

why do we even need : (i += 1) syntax? wouldnt it be better to just remove all those nuances @SpexGuy delineated? to minimize energy spent on coding style @andrewrk

Was this page helpful?
0 / 5 - 0 ratings