There's probably more I could flesh out about this proposal but I'd like to get it out there, so here goes:
Synopsisfor
loops are only really useful for slices, and even for them, it's better to use a while
because you may not be iterating a slice in the future, as your program matures.
However, while
loops have some shortcomings over for
loops, being more verbose, and no easier to understand.
For example, a std.ArrayList
called list
:
// this is what the iterator of std.ArrayList does: var i: usize = 0; while (i < list.len) : (i += 1) { const elem = list.at(i); // do something with each element } // ----- versus ----- // this doesn't work currently. for (list) |elem| { // do something with elem }
Currently, in order to make a struct iterable, convention is that you have an .iterator()
method that returns an instance of a iterator struct for that type, and you then call .next()
on that iterator until it returns null
.
Again, for a std.ArrayList
called list
:
var itr = list.iterator(); while (itr.next()) |elem| { // do something with elem }
This approach is arguably a tad better than the while
example above; more terse, pretty clear.
However it is a little awkward because, it's different from how you iterate slices with for
--the only type for
can iterate--and the author of the custom struct (here std.ArrayList
) ends up writing dozens of lines of code just to make the iterator struct work as it should... when all you wanted to do was write the code necessary for iterating this collection and reuse that code whenever you want to iterate one of them.
Here is that code for std.ArrayList
:
pub const Iterator = struct { list: *const Self, // how many items have we returned count: usize, pub fn next(it: *Iterator) ?T { if (it.count >= it.list.len) return null; const val = it.list.at(it.count); it.count += 1; return val; } pub fn reset(it: *Iterator) void { it.count = 0; } }; pub fn iterator(self: *const Self) Iterator { return Iterator{ .list = self, .count = 0, }; }
For std.ArrayList
, it's not that bad; a total of 23 lines.
However, this code looks very different from the code you actually need to write in order to iterate this list - it's from the first example - here it is again:
var i: usize = 0; while (i < list.len) : (i += 1) { const elem = list.at(i); // do something with each element }
That's pretty simple, right? It's only 5 lines!
I'd argue that having to use a custom struct just to iterate your actually-important custom struct is something of a waste of your time - it shouldn't be that hard.
It should not require that many lines more code to write it, ideally.
And again, this is just an ArrayList
- the custom iterator code for a BucketArray
is worse, and harder to follow than this is.
This proposal fixes both of these problems in one swoop.
BasicsIt makes it so that you can just for (iterable) |elem| { ... }
to iterate over any custom struct that declares how you iterate it.
Iterating would then look like this:
// array_list.zig ---- pub fn iterate(self: *Self, f: fn(e: ElementType) void) void { var i: usize = 0; while (i < self.len) : (i += 1) { @inlineCall(f, self.at(i)); } } // main.zig ---- const std = @import("std"); pub fn main() !void { var a = std.debug.global_allocator; var list = std.ArrayList(i32).init(a); try list.append(1); try list.append(2); try list.append(4); var x: i32 = 0; for (list) |e| { x += e; std.debug.warn("{}\n", e); // prints each element } std.debug.warn("x is {}\n", x); // prints 7 (1+2+4) }
Behavior-wise, this setup pastes the code from the body of iterate
into main
, replacing the for
loop, and so, local variables in main
are available in the loop.
The f
function parameter to iterate
is how you represent the body of the users' for
loop.
No function calls, either to iterate
or f
, actually happens.
This is actually the same amount of magic as currently happens with for
loops,
the only difference is that you are able to specify what to do for an abitrary custom structure,
rather than it only working for slices.
Also, it's as if you just wrote this:
pub fn main() !void { var a = std.debug.global_allocator; var list = std.ArrayList(i32).init(a); try list.append(1); try list.append(2); try list.append(4); var x: i32 = 0; { var i: usize = 0; while (i < list.len) : (i += 1) { const e = list.at(i); x += e; std.debug.warn("{}\n", e); // prints each element } } std.debug.warn("x is {}\n", x); // prints 7 (1+2+4) }
Notice how close the loop is to the body of iterate
.
Futher, sometimes, though rarely, the iteration can fail.
For example, command line arguments allocate each arg as you ask for them:
Currently, you can use while
on an error union and have an else
branch to handle the error.
This code will repeatedly evaluate the expression until it's error.
It will then run the else
branch:
while (getAnswerOrErrorIfAtTheEnd()) |answer| { // do something with the answer } else |err| { // do something with the error. }
The same applies here.
If the iteration results in an error, you'd be forced by the compiler to provide an else
branch, and that would look like this:
var args = std.process.args(allocator); for (args) |arg| { // do something with the argument } else |err| { // do something with the error }
Notice any similaraties? 😝
To allow for this case, the implementation of iterate
for the custom struct could return either void
if it always suceeds, or !void
if it can fail.
There is an open question with this approach.
How would you remove an item from the iterable, while you are iterating over it.
If you don't think you'd need to, see std.ArrayList.swapRemove
. Trust me, it's useful. 😁
A little while ago, I made a PR which added .swapRemove
and .orderedRemove
to the custom iterator type of std.ArrayList
.
If you skip needing to have an iterator, how would you access that functionality in a simple way without having to concern yourself over how you actually do that for the data structure in question, or figuring out how to even do it at all, as applicable.
Jai solves this problem by having a remove
keyword which you provide the value to.
The Zig translation of that is this:
for (list) |elem| { if (elem < 2) continue; remove elem; }
I'm not sure of the best way to do this, though this is one way I thought of:
pub fn iterate(self: *Self, f: fn(e: ElementType) void) void { var i: usize = 0; while (i < self.len) : (i += 1) { @inlineCall(f, self.at(i)); // what types of removal you do is comptime-known and // you can emit a compile error if someone tried to orderedRemove // from a structure that cannot do that, for instance. switch (@removalType()) { .Fast => { _ = self.swapRemove(i); i -= 1; // incremented at end of loop; so decrement it here. // the next item appears in the old items position after the remove, // so use the same index again. }, .KeepOrder => { _ = self.orderedRemove(i); i -= 1; }, .None => continue, } } } const std = @import("std"); const warn = std.debug.warn; pub fn main() !void { var a = std.debug.global_allocator; var list = std.ArrayList(i32).init(a); try list.append(1); try list.append(2); try list.append(4); for (list) |e| { if (e == 2) { remove e; // remove_ordered e; } std.debug.warn("{}\n", e); // prints each element } assert(list.len == 2); assert(list.at(0) == 1); assert(list.at(1) == 4); // because swapRemove. }
mikdusan, CurtisFenner, ikskuh, uael, pixelherodev and 6 moreandersfr and GoNZooohamad-almamari
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4