Closed AndreaOrru closed 5 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.
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.
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.
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.
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
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;
}
}
}
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.
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
Also to specify the type @ElectricCoffee , like @range(u8, 0, 10)
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
I made a comptime range fn for anyone interested: https://gist.github.com/hazeycode/e7e2d81ea2b5b9137502cfe04541080e
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.
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...
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?
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
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.
Where
a
andb
can be chars, integers, anything that can define a range. This is also better syntax IMHO than: