ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
33.27k stars 2.42k forks source link

Proposal: for on ranges #358

Closed AndreaOrru closed 5 years ago

AndreaOrru commented 7 years ago
for (a...b) |x, index| {
    ...
}

Where a and b can be chars, integers, anything that can define a range. This is also better syntax IMHO than:

{var i = 0; while (i < n; i += 1) {
    ...
}}
nodefish commented 3 years ago

@EleanorNB

Dude's been here for years, please don't be condescending.

Please reconsider your uncharitable reading of what I wrote. I will leave it at that.

@ifreund

I see, thanks for the clarification. That is rather subtle. In the most common cases it'll be a similar distinction to using < as opposed to <= in the condition expression, so I think I will still use the defer approach when possible and keep the extra incrementation in mind, just as a matter of personal preference.

thejoshwolfe commented 3 years ago

You can do range iterators in userland:

fn range(times: usize) RangeIterator {
    return RangeIterator{
        .cursor = 0,
        .stop = times,
    };
}

const RangeIterator = struct {
    cursor: usize,
    stop: usize,

    pub fn next(self: *RangeIterator) ?usize {
        if (self.cursor < self.stop) {
            defer self.cursor += 1;
            return self.cursor;
        }
        return null;
    }
};

However, I did some optimization science in godbolt, and i believe there is some optimization benefit to having some kind of builtin range loop.

Call a function N times

Baseline status quo: https://godbolt.org/z/rdYMq4

export fn callSomethingNTimes(num: usize) void {
    var i: usize = 0;
    while (i < num) : (i += 1) {
        something(i);
    }
}

More convenient syntax using a userland iterator: https://godbolt.org/z/98G5d3

export fn callSomethingNTimes(num: usize) void {
    var it = range(num);
    while (it.next()) |i| {
        something(i);
    }
}

The output is slightly different, but I'm not an expert enough to know which one is better.

Do math with the iterator variable

using iterator integer: https://godbolt.org/z/Y5Pon5

export fn mathThing(num: usize) usize {
    var sum: usize = 0;
    var i: usize = 0;
    while (i < num) : (i += 1) {
        sum +%= i * i;
    }
    return sum;
}

using iterator object: https://godbolt.org/z/Gh7MPc

export fn mathThing(num: usize) usize {
    var sum: usize = 0;
    var it = range(num);
    while (it.next()) |i| {
        sum +%= i * i;
    }
    return sum;
}

The output looks very different for these two, which declares the iterator integer the clear winner in terms of optimizability.

So it's not as clear cut as "just use a userland iterator object", but the option is still there.

andrewrk commented 3 years ago

The output looks very different for these two, which declares the iterator integer the clear winner in terms of optimizability.

So it's not as clear cut as "just use a userland iterator object", but the option is still there.

see also the -OReleaseSmall optimization mode for these examples which is quite interesting

Srekel commented 3 years ago

The fundamental issue is: almost every single time you want to iterate over a range in real code, it's actually to index into a data structure.

@EleanorNB I don't agree with this. In game development it is quite common to loop over ranges that aren't bound to an exact data structure. Often because the loop values aren't actually indices, but coordinates, for example. Useful when generating meshes.

Or you may have a data structure, but you may want to sample over it in weird ways, for example a 2d map in a circular pattern.

And it is often two-dimensional, sometimes three, and that makes using the while-loop style @andrewrk suggested cumbersome.

Not having a programmer-friendly way to write for loops would I think be quite alienating to game programmers. I'm not sure I see the upside. The appeal of Zig's current for loops is very appealing as-is, I will still use them when I can.

[Edit: My comment sounded a bit rude/snarky, made a couple edits as it wasn't my intention]

Here's some typical code I've written, taken from the map generation code for Hammerting:


  // Fix foreground in an oval area near the start
  f32 width_sq  = flatten_width * flatten_width;
  f32 height_sq = flatten_height * flatten_height;
  for ( f32 y = -flatten_height; y < flatten_height; y++ ) {
    for ( f32 x = -flatten_width; x < flatten_width; x++ ) {
      f32 ellipse_eq = x * x / width_sq + y * y / height_sq;
      if ( ellipse_eq >= 1 ) {
        continue;
      }

      double influence_x = 1 - wc::abs( x ) / flatten_width;
      double influence_y = 1 - wc::abs( y ) / flatten_height;
      double  influence = influence_x * influence_y;
      i32     map_index = room_pos._x + i32( x ) + ( room_pos._y + i32( y ) ) * i32( MAP_WIDTH );
      double& heightmap_value = heightmap[map_index];
      heightmap_value += ( y <= 0 ? 2 : -2 ) * influence * influence;
      if ( heightmap_value < HEIGTMAP_CUTOFF ) {
        out->_foreground._materials[map_index] = 0;
      }
      else {
        u8 mat                                 = out->_background._materials[map_index];
        out->_foreground._materials[map_index] = mat;
      }
    }
  }
BinaryWarlock commented 3 years ago

The fundamental issue is: almost every single time you want to iterate over a range in real code, it's actually to index into a data structure

That's simply not true, I've read mountains of "real code" that does this all the time. Again, I do not see how experienced C programmers have not encountered this more often, because it's seemingly everywhere. Perhaps we are just looking at vastly different types of projects.

Perhaps take a stroll through some mathematics, game development, or even OS code to see some real world examples of this.

Writing a RangeIterator is more verbose (and slower) than just using the status-quo while loop, which is no good.

This problem is exacerbated by not being able to shadow locals, and making a new block scope just for iteration is awful as mentioned. This is a super unergonomic case in Zig right now and really something should be done about it, whether that be macros (#6965), or this, or another solution is up to debate, but it is unhelpful to close it with "the status quo is fine" when it is clearly not fine, or it would not still be debated 4 years later(!)

Remember that every developer has different use cases, so although you may not see this as a big deal, others definitely do. And this is a simple and elegant solution to the problem, so I don't see why there is so much pushback on it.

ElectricCoffee commented 3 years ago

Getting some form of range functionality would be really nice, if not for readability, then for writeability.

Surely it's possible to have the compiler build an array at compile-time or return a slice at runtime in order to facilitate something like this:

for (@range(2, 8)) |i| {
    // do stuff
}

Maybe even leverage anonymous structs somehow to facilitate optional arguments:

for (@range(2, 8, .{ .by = 2, .inclusive = true })) |n| {
    // you get it
}

It's not as elegant as the equivalent in Python, but it's at least clear in its intent and much harder to mess up than a c-style for loop or a plain while with a defer

Guigui220D commented 3 years ago

Also to specify the type @ElectricCoffee , like @range(u8, 0, 10)

ElectricCoffee commented 3 years ago

Also to specify the type @ElectricCoffee , like @range(u8, 0, 10)

I didn't even think of that, but yeah, that's a good logical addition also

hazeycode commented 2 years ago

I made a comptime range fn for anyone interested: https://gist.github.com/hazeycode/e7e2d81ea2b5b9137502cfe04541080e

gonzus commented 2 years ago

If we are still looking for a range notation, I find this quite readable and it covers all the cases:

a =..< b  // includes a, excludes b; most used case?
a =..= b  // includes a, includes b
a >..= b  // excludes a, includes b
a >..< b  // excludes a, excludes b

You could even have a .. b as shorthand, equivalent to the first case.

dbechrd commented 1 year ago

If we are still looking for a range notation, I find this quite readable and it covers all the cases:

a =..< b  // includes a, excludes b; most used case?
a =..= b  // includes a, includes b
a >..= b  // excludes a, includes b
a >..< b  // excludes a, excludes b

You could even have a .. b as shorthand, equivalent to the first case.

I assume you meant your > to be < on the left side? Your suggestion would make significantly more sense that way:

a =..< b  // equal to less than
a =..= b  // equal to equal
a <..= b  // greater than to equal
a <..< b  // greater than to less than

That said.. exclusive start seems pretty bizarre to me, so why not just:

a ..< b  // exclusive
a ..= b  // inclusive

Edit: I just looked up Odin's loop syntax and realized this is pretty much exactly how Odin works. Genuinely was not aware of that, but alas...

gonzus commented 1 year ago

Yes, I meant that, brain fart.

I now realise this issue is closed, and I am not sure if any decision was made about this; anybody knows?

Vexu commented 1 year ago

7257 is accepted and will add for loops on ranges.

andrewrk commented 1 year ago

Multi-object for loops have landed with #14671, and now for loop syntax supports counters:

const std = @import("std");

pub fn main() !void {
    for (0..10) |i| {
        std.debug.print("{d}\n", .{i});
    }
}
$ zig run test.zig 
0
1
2
3
4
5
6
7
8
9
jkellogg01 commented 5 months ago

Are there any plans for inclusive counters as a convenience? obviously the same functionality is achievable by just increasing the end number by one but I would argue there's a readability benefit to the counter being able to specify that it's inclusive.

I know there have been a bunch of suggestions in terms of formatting, but I haven't seen a mention of the fact that Rust already uses the x..y syntax for exclusive ranges and simply uses x..=y to specify inclusive. The benefit to borrowing this syntax would be that the existing counter syntax wouldn't need to change at all.