Int iterator

I was getting so tired of some loops thatt I wrote this… Not perfectly crashproof but workable.

pub fn IntIterator(comptime T: type) type {

    return struct {
        const Self = @This();

        from: T,
        to: T, // exclusive
        current: T,

        pub fn init(from: T, to: T) Self {
            return .{
                .from = from,
                .to = to,
                .current = from,
            };
        }

        pub fn next(self: *Self) ?T {
            if (self.current >= self.to) {
                return null;
            }
            defer self.current += 1;
            return self.current;
        }
    };
}
    var iter = IntIterator(i32).init(-10, 12);

    while (iter.next()) |v| {
        std.debug.print("{}, ",. { v });
    }
3 Likes

It’s a trivial construct, but I bet such APIs can be useful for some less trivial flows where a simple for is insufficient. By the way, your .from is unused and can be removed :slight_smile:

1 Like

Yep it is trivial. But it already saved me from “off by one” bugs.

1 Like
test "int iterator" {
    loop: switch (@as(i32, -10)) {
        12 => {},
        else => |i| {
            std.debug.print("{}, ",. { i });
            continue :loop i + 1;
        },
    }
}
6 Likes

:slight_smile: I did not run it, but doesnt 12 need a break :loop?

It’s a switch, so it ends implicitly.

2 Likes

Crazy… :slight_smile:
I wonder how the 3 versions compile (raw for, iter, loop switch)

Not raw integers but i used iterator for producing 2d coordinates. https://codeberg.org/andrewkraevskii/steam_train/src/commit/e0248bee3cbfce75d25da73f28c1228acf160195/src/main.zig#L475

i lke that one.

wait, this code runs, how is this possible? i thought continue only works inside for and while blocks?

zig lang reference states

continue:  
`continue` can be used in a loop to jump back to the beginning of the loop.
2 Likes

I think this is interesting, kinda reminds me of the range() in python. It would be cool if this could perform decrement or with steps:

IntIterator(i32).init(12, -10); // from 12 to -10
IntIterator(i32).initStep(-10, 12, 2); // from 12 to -10, with increment of 2

The continue jump is really fun especially with switch, and this is some of my favorite features since 0.16.0; though, isn’t this solution can only support iteration in a fixed range, which in this case, it can only perform -10 → 12 iteration? Besides, if I have some function calls, doesn’t it suggest that we need to put the function calls under the else block?

while is easier at this point:

test "int iterator" {
    const from: i32 = -11;
    var to: i32 = 13;
    to += 0;
    const step: i32 = 2;

    loop: switch (from) {
        else => |i| {
            if (i == to) break :loop;
            std.debug.print("{}, ",. { i });
            continue :loop i + step;
        },
    }

    std.debug.print("\n",. {});

    {
        var i = from;
        while (i != to) : (i += step) {
            std.debug.print("{}, ",. { i });
        }
    }
}

std.debug.print() is a function call. You can call as many functions as you like before continue.

Yeah, I think simplicity wins here since the labelled switch ends up becoming another style of iteration rather than an iterator which is similar yet with subtle difference despite a cool behavior.

For me, iterator provides an abstraction to how index or pointer are moved in loops so that we can iterate a collection without manually update the index, something like this:

var iter = Iter.init(...); // Depends on the type: it can be a range or a collection.

// yay! we don't need to handle the indexing since the iterator handles it
while (iter.next()) |iter_data|{
    ...
}

The pattern from the Op was extremely useful for one of my problems in a gui application where I need to handle a waveform canvas where the mouse may draw forward and backward. However, I didn’t thought of the iterator approach, I ended up swapping the numbers just to comply the for loop:

var sample_idx_start: usize = @intFromFloat(@round(std.math.clamp((idx_start / waveform_canvas_rect.w) * sample_frame_len, 0, sample_frame_len - 1)));
var sample_idx_end: usize = @intFromFloat(@round(std.math.clamp(idx_end / waveform_canvas_rect.w * sample_frame_len, 0, sample_frame_len)));

// we shall retain the orientation of the starting and ending index;
// otherwise, zig will crash if the ending index is smaller.
if (idx_end < idx_start) {
    std.mem.swap(usize, &sample_idx_start, &sample_idx_end);
}

const intepret_delta = (mag_end - mag_start) / @as(f32, @floatFromInt((sample_idx_end - sample_idx_start)));

for (sample_idx_start..sample_idx_end, 0..) |idx, i| {
    const interpret_y = mag_start + @as(f32, @floatFromInt(i)) * intepret_delta;
    frame_group.getSelectTimeFrame().?[idx].re = std.math.clamp(interpret_y, -1, 1);
}

With Op’s pattern, I could simply create an iterator with return an iteration struct containing both required indices:

var sample_idx_start: usize = @intFromFloat(@round(std.math.clamp((idx_start / waveform_canvas_rect.w) * sample_frame_len, 0, sample_frame_len - 1)));
var sample_idx_end: usize = @intFromFloat(@round(std.math.clamp(idx_end / waveform_canvas_rect.w * sample_frame_len, 0, sample_frame_len)));

const intepret_delta = (mag_end - mag_start) / @as(f32, @floatFromInt((sample_idx_end - sample_idx_start)));

// it could return a struct for both array index and the interpreted sample amount
var iter = IntIter.init(sample_idx_start, sample_idx_end, intepret_delta);
while (iter.next()) |item| {
    frame_group.getSelectTimeFrame().?[item.idx].re = std.math.clamp(item.interpret_y, -1, 1);
}

This also suggests I could generalize the solution with a single iterator type that provide the start, end indices and the delta of each steps, instead of writing multiple iteration of the same kind with slight variant (which was a problem I faced in that project). I also no longer need to use comment to explain why I suddenly do a memory swap just to reminds not to accidentally remove the swap in the future commits.

1 Like

Yep. My example was of course a simple one. It can be extended doing more like reverse, steps, returning converted data etc.
Too bad we cannot have 2 or more whiles int the same way as the Zig for loop.

Yeah, that’s a bit unfortunate, but that’s okay since we can still call the next iterator right after the while loop, so for me, this workaround is good enough despite not perfect:

const a = IntIterator(i32).init(-8, 13);
const b = IntIterator(i32).init(0, 21);

while (a.next()) |i| {
    const j = b.next() orelse break;
    ...
}

True. That was the best thing I could think of as well.