For-loop counter other than usize

You can use a screwdriver to hammer nails. That doesn’t mean the screwdriver was intended for hammering nails.

Everything about the for loop and its restrictions sure suggest that it’s explicitly designed for iterating over array elements in order. Your ranges can’t have negative bounds, you can’t change the step size or iterate in reverse and your captured variables are usize integers. If Zig range expressions were intended for general counting, they would probably resemble a Lua-style for i=start,end,step loop more closely than they do today.

However, it’s possible you might get something similar to what you ask for if/when allow integer types to be any range · Issue #3806 · ziglang/zig · GitHub (which the compiler team has expressed great interest in) is implemented. This is all conjecture, but presumably, once implemented, a range expression like n..m could be captured as a variable of type

@Int(
    @min(math.minInt(@TypeOf(n)), math.minInt(@TypeOf(n))),
    @max(math.maxInt(@TypeOf(n)), math.maxInt(@TypeOf(m))) + 1,
)

which when the bounds are comptime-known like in 0..10 will be @Int(0, 10), which coerces to wider integers including u8 (which will be an alias for @Int(0, 256)). Negative bounds might still be off-limit.

This is directly from the documentation:

   // To iterate over consecutive integers, use the range syntax.
    // Unbounded range is always a compile error.
    var sum3 : usize = 0;
    for (0..5) |i| {
        sum3 += i;
    }
    try expect(sum3 == 10);

You will note that this is a general N to M counting task. Not an array in sight.

That would only help if you can specify… wait for it… a for loop value type :sunglasses: that isn’t usize!.

I don’t actually see a strong benefit in integer range types existing, where for loops is concerned. You can specify the range already, all that’s left is for the type to be chosen by the user and not the language.

But the integer range type is a good idea for other reasons. Any program invariant that can be offloaded to the type system should be. Expressing a weekday as a u3 still means one illegal value, none would be better. You’d probably use an enum there, but then ?Weekday can fit in three bits when you pack it if it’s a u1:7 or whatever they end up calling it.

I wonder if iterator isn’t the one exception regarding hidden flow that’s worth making. Having it in a named variable doesn’t enable you to do anything most of the time. The iterator is going to be modified exclusively through next() anyway.

Looking at what’s happening with Go, IMO iterators are one of the least expressiveness-bang for complexity-buck of all abstractions of this kind. Just call .next() and live happily everafter.

gingerBill wrote a blog post about this stuff recently Why People are Angry over Go 1.23 Iterators - gingerBill

That’s kind of a side effect of the recent rework of for loops to support iterating on multiple sequences at the same time (which also made the index an explicit argument). Abstract arguments about what something was designed to solve or not can be flaky but in this case I would tend to agree that the idea is “for for basic looping, while for everything else”.

More on that change https://kristoff.it/blog/zig-multi-sequence-for-loops/

2 Likes

This much I agree with, to quote myself on that very subject:

I don’t think being able to write code such as

for (-7..7) |i: i16| {
    // ...
}

Would spoil the Zen of the language. It completes the process which started with adding ranged for loops.

It adds one more pinch of syntax, one which has only one possible interpretation, and in return, we get something which is fully general. I don’t care for the whiff of the arbitrary in language features, I figure that since we have ranged for loops, they should be able to be ranged over whatever values, using whatever type, the user wants.

To reiterate a point (not quoting myself twice!) it’s perfectly reasonable for the core team to not want to direct effort toward this. I would have preferred to see the issue stay open, in the event that someone decided to tackle it on their own, but having open issues itself imposes a certain maintenance burden, so I understand that choice as well.

If and when ranged integer types get added, that opens the door to a very nice feature from Pascal and Ada, namely arrays of a ranged type. Those can start and stop at any offset, so you could have something like [-7..7]u64 or what have you. Since the compiler would enforce that they only be indexed with the associated range type, there’s no need for bounds checking either. Coercion to a slice is… a bit of a wrinkle, admittedly, although just slicing it doesn’t have the same problem.

That would be great, and it basically does require the full for range specification to get full use out of it. So there may be an opportunity to revisit the question down the road.

1 Like