-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Proposal: for on ranges #358
Comments
Previously when @andrewrk and I considered this syntax very early in Zig's development, one problematic point was the upperbound inclusivity/exclusivity. With an expression like Surely the question can have an answer, and it should probably be exclusive, but the point is that the syntax doesn't clearly say that it's exclusive. And just to confuse things, I've seen languages (coco, for example) use two different syntaxes for I don't think users will have a very good success rate for guessing whether the upper bound is inclusive or exclusive, which makes me dislike this proposal. That being said, iterating over numbers |
I wouldn't be against the two different tokens, but intuitively I'd say If we only keep |
We do have the On the other hand, an unsolved problem is that in switch statements, switch (c) {
'a'...'z' => {}, // inclusive
} I have 2 ideas to solve this problem:
|
I'm not sure how I feel about using the |
I posted this in #359, just adding the relevant part here, which suggests keeping the two range operators
|
i haven't seen mention of it yet, so i'll just do so myself: is there a way to count backwards? will |
It was mentioned before, the expected behaviour for that statement would be it would actually loop 0 times - ie, the block would not execute. To count backwards, you'd do |
I dislike this proposal,it is difficult to know it's meaning without read the document. A function call may be better. |
Care to elaborate? Which document? What function call? Can we see an example? |
from @Hejsil question is if the same can be hacked together for ranges not starting at 0 |
I dislike this proposal:
what a and b mean? is a include? is b include? what is x mean? what is index mean? If the lang design like this, I have to read the document to understand what it means. I am looking for something more understandable syntax, may be like this?:
|
this gets you most of the way there:
|
We already have the concept of ranges in slices and in switch statements - for this proposal to be reasonable, we just need to be consistent throughout the language. I know we're optimizing for readability, but we should be able to expect that a person reading Zig code has at least looked through the Zig docs. That times function returning a In @bronze1man's proposal, an assignment would have to be an expression that returns whatever was assigned. It might also be unclear that the function only runs once - a reader might expect each iteration of the loop to declare a new variable i set to the result of a new call. Zig had a "null-unwrap-and-declare" operator
|
I think there is a number of things that needs to be thought of here:
If for loops work on slices, arrays and ranges, it's starting to get confusing. What's the pattern? Could it be generalized in a meaningful way? I gotta say I'm a big fan of languages where the for loop accepts some kind of "iterator", rather than a few special types. It makes it much more clear and explicit what's going on, and easier to read in my opinion. So if you have an array
And then for ranges you could do:
But how you make those iterators work is a huge proposal on its own. |
if only there were something called interfaces ore alike 🥇 |
Iterators can be done with Line 155 in 02713e8
|
compare
to
I do not think the first one enhances clarity. And iterating is one of the most common things so there is that ... with interfaces its possible to have concise iterators
sure but whats the point? (apart that recursion is currently not working and should be avoided) |
I can't even remember those two (and they look very similar as well) while I find the ruby syntax intuitive so it really depends on the person... In the end you just have to remember some syntax so I do not think this is actually such a big deal. The issue is a very restricted for loop and not a |
👍 for : without a variant form since the intent is very clear and unambiguous among a number of languages. Might be nice to add optional stride too ;) |
I'm not sure it's a good idea to couple syntax with the names that are defined in a struct - saying you can use
Yep. The while iterator pattern is both explicit and concise - if you end up having to change how the iterator has to be initialized or continued, the function calls aren't hidden behind the syntax. If the value of this proposal is good, and the only concerns are regarding syntax, we could accomplish this with built in functions and call it as Example names and signatures. If it's hard to work out what they mean, it's a sign we need better functions.
Can built in functions be async/generators?
|
@raulgrell I think if those built-ins end up being needed, it's a failure of language design. Built-ins should be functionality that can not in any way be implemented with library code. There are many of ways to design the language such that these can be implemented as plain code rather than magic built-ins, and I'm sure one of them can keep Zig conceptually simple and explicit. I agree that there shouldn't be special function calls generated by the I don't really like having to use while-loops to use an iterator pattern, but when I think about it, it's probably the correct choice for Zig. Is there a proposal for generators (or observable or whatever it should be called)? It would make a lot of sense to extend the async support to allow an async function to yield multiple values. Then it would also make sense to extend for-loops to support those. |
FWIW you can always increment a variable with while(...) {
defer i = i + 1;
} |
This does not have the same semantics as the |
Consider the following snippet: const std = @import("std");
const warn = std.debug.warn;
pub fn main() void {
var i: u8 = 0;
while (i < 10) {
defer i += 1;
warn("{}\n", .{i});
}
} The output is:
It's easy to overlook but each iteration of a loop has its own frame/scope. In golang you'd be right but defer applies to the immediate scope in zig. |
const std = @import("std");
const print = std.debug.print;
pub fn main() void {
print("loop 1:\n", .{});
var i: u8 = 0;
while (i < 10) {
defer i += 1;
print("{}\n", .{i});
if (i == 5) break;
}
print("{}\n", .{i});
print("loop 2:\n", .{});
i = 0;
while (i < 10) : (i += 1) {
print("{}\n", .{i});
if (i == 5) break;
}
print("{}\n", .{i});
}
|
@nodefish Consider the following snippet: const std = @import("std");
const info = std.log.info;
pub fn main() void {
var i: u8 = 0;
while (i < 10) {
defer i += 1;
break;
}
warn("{}", .{i});
} It prints 1. @MasterQ32 See, I would have expected that to be an exclusive range, as that's how @cryptocode No good -- 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. The sensible way to structure your code is to iterate over the structure directly, and the lack of ranged Really, the problem is that |
@EleanorNB I agree with the range point of view, I'm just addressing the scoping issue. I don't see people adding additional {} scopes in complex/nested loops, leaving the door open to subtle bugs. I see how the ; sequence means a difference syntax is needed, but that's orthogonal. |
@EleanorNB
Please reconsider your uncharitable reading of what I wrote. I will leave it at that. I see, thanks for the clarification. That is rather subtle. In the most common cases it'll be a similar distinction to using |
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 timesBaseline 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 variableusing 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. |
see also the |
@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;
}
}
} |
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 |
Also to specify the type @ElectricCoffee , like |
I didn't even think of that, but yeah, that's a good logical addition also |
…f while Using `while` with a counter is a code smell for "maybe you ought to be iterating a data structure instead". Using a slice also avoids repeated bounds-checks on each execution of the loop body in safe compile modes. Related discussion (and why Zig does not have a for range syntax: ziglang/zig#358 (comment)
I made a comptime range fn for anyone interested: |
If we are still looking for a range notation, I find this quite readable and it covers all the cases:
You could even have |
I assume you meant your
That said.. exclusive start seems pretty bizarre to me, so why not just:
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? |
#7257 is accepted and will add for loops on ranges. |
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});
}
}
|
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 |
I agree and I feel like the simplest way to prove the point is to try and reduce the number of options required whether or not a/b is inclusive or not. Simply take the position that A is always inclusive and just specify if B is exclusive or inclusive. You can basically always assume that A is going to be inclusive because otherwise you would just move the start position to where you actually want to start. But you cant really move the end position beyond something like a list From a human & coding perspective, saying "loop over everything in this box plus one" is alot harder to understand conceptually than "loop over everything except the first item in this box". Include A, Exclude B
Include A, Include B
Exclude A, Exclude B
Exclude A, Include B
|
Where
a
andb
can be chars, integers, anything that can define a range. This is also better syntax IMHO than:The text was updated successfully, but these errors were encountered: