A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/ziglang/zig/issues/3110 below:

Streamline loops, and enhance iteration · Issue #3110 · ziglang/zig · GitHub

There's probably more I could flesh out about this proposal but I'd like to get it out there, so here goes:

Synopsis

for 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.

Basics

It 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.

Failure

Futher, sometimes, though rarely, the iteration can fail.
For example, command line arguments allocate each arg as you ask for them:

fn internalNext(self: *ArgIteratorWindows, allocator: *Allocator) NextError![]u8 { var buf = try Buffer.initSize(allocator, 0); defer buf.deinit();
pub fn next(self: *ArgIteratorWindows, allocator: *Allocator) ?(NextError![]u8) { // march forward over whitespace while (true) : (self.index += 1) { const byte = self.cmd_line[self.index]; switch (byte) { 0 => return null, ' ', '\t' => continue, else => break, } } return self.internalNext(allocator);

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.

Element removal

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