Skip to content

a place to put variables in only while loop scope #5070

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
anosovic opened this issue Apr 16, 2020 · 20 comments
Closed

a place to put variables in only while loop scope #5070

anosovic opened this issue Apr 16, 2020 · 20 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@anosovic
Copy link

anosovic commented Apr 16, 2020

   {
        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?

@pixelherodev
Copy link
Contributor

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

@anosovic
Copy link
Author

anosovic commented Apr 16, 2020

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

@Vexu Vexu added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Apr 17, 2020
@Vexu Vexu added this to the 0.7.0 milestone Apr 17, 2020
@karrick
Copy link

karrick commented Apr 18, 2020

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);
}

@jakwings
Copy link

jakwings commented Apr 18, 2020

It becomes worse when we add more variables... 😉 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

@jakwings
Copy link

jakwings commented Apr 18, 2020

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

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

@jakwings
Copy link

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.

@aeronavery
Copy link

aeronavery commented Apr 19, 2020

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| {

}

@SpexGuy
Copy link
Contributor

SpexGuy commented Apr 19, 2020

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.

@DanB91
Copy link

DanB91 commented Apr 21, 2020

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.

@anosovic
Copy link
Author

anosovic commented Apr 21, 2020

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) {}

@DanB91
Copy link

DanB91 commented Apr 21, 2020

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.

@SpexGuy
Copy link
Contributor

SpexGuy commented Apr 21, 2020

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

@andrewrk
Copy link
Member

Use anonymous blocks to limit the scope of variables.

@markfirmware
Copy link
Contributor

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) {
       ...
   }
}

@marler8997
Copy link
Contributor

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.

@ghost
Copy link

ghost commented Apr 21, 2020

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?

@andrewrk
Copy link
Member

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();
}}

@karrick
Copy link

karrick commented Apr 21, 2020

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.

@jakwings
Copy link

jakwings commented Apr 21, 2020

{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);

@anosovic
Copy link
Author

anosovic commented Jun 15, 2020

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

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

12 participants