Loops and types

Hello fellow programmers,
I think I read something about types loops in the language.
Is there any chance “typed loops” will be in the language?
What are the general thoughts here?

Typecasting needed:

const start: u16 = 42;
const end: u16 = 64;
for (start..end) |value|
{
    // value is an usize now.
}

Also I think an inclusive loop is not possible:
for (start..=end) |value|

Also this is not possible.

for (-10..10) |i|
{
    std.debug.print("{}", .{i});
}

I like the c and c# like loops more. Full control and fully typed.

for (int i = 0; i < 42; i++)
{
}

BTW: what I like very much in Zig is the “parallel loop” on two arrays.

for (items_a, items_b) |a, b|
{
}
3 Likes

No. Not much information given as to why, other than it seems to cause technical problems.

This is definitely at the top of the list for me when it comes to features that Zig is lacking.
It happens soo many times that I want an i32 or a u16 in a for loop.

5 Likes

For me too. I like typed.
(btw: anytype is also very untyped, that;s why i dont like that one, but i can understand the implications)

It is possible to do something similiar to what you’re looking for in comptime like this:

const std = @import("std");
const assert = std.debug.assert;

fn Range(comptime from: comptime_int, comptime to: comptime_int) type {
    const Int = std.math.IntFittingRange(from, to);
    const len = @abs(to - from);
    return [len]Int;
}

inline fn range(comptime from: comptime_int, comptime to: comptime_int) Range(from, to) {
    comptime {
        var arr: Range(from, to) = undefined;
        const d = if (from > to) -1 else 1;
        var val = from;
        for (0..arr.len) |i| {
            arr[i] = val;
            val += d;
        }
        assert(val == to);
        return arr;
    }
}

export fn loop(a: i32, b: i32) i32 {
    var res: i32 = 0;
    for (range(-30, 127)) |i| {
        comptime assert(@TypeOf(i) == i8);
        res += a;
        res *= b;
    }
    return res;
}

(godbolt)

This produces sensible assembly (I checked for x64 and aarch64 at least) and a properly typed index. My example only works for comptime-known ranges though.

EDIT: Scratch that it doesn’t work at all (see reply)

Only because you aren’t actually using i anywhere in the godbolt example. If you include it in the calculations, you will see that it starts loading from memory, which is definitely not desirable.

2 Likes

Yeah you’re right, my bad. I guess it still works if you’re willing to inline your loop.

I’m always perplexed when I see discussions like this. What’s wrong with just a while loop. I consider the for statement as specialized for iterating over indexed entities. while provides all that I’ve ever needed if I’m doing other things. Am I missing something here?

5 Likes

while is ok :slight_smile:

1 Like

IMHO the (quite obvious) main problem with Zig’s while loop as a replacement for a for-loop is that the loop variable needs to be declared outside the while block and thus ‘pollutes’ the outer scope:

var i: usize = 0;
while (i < 10): (i += 1)  { ... }

FWIW I’m also very much in favour of a more flexible for-loop that doesn’t hardwire the loop variable to usize, but either allows to define the type manually, or delays the ‘type resolution’ until the loop counter is actually used in an expression.

5 Likes

But indices don’t have to be usize every time especially with Zig explicitly promoting data oriented design using a u32 to index data is pretty common. And if for was exclusively meant to iterate slices there wouldn’t be any need for the (0..n) range syntax altogether. It’s just a pretty frequent inconvenience and a source of many unnecessary @intCasts imo.

5 Likes

for (0..10) |i: i32| { .. } would be nice tbh… in such for-loops its also quite unlikely that the loop variable will actually be used as an index into an array. Question is just how far that would go… would for (0..10) |i: f32| be allowed? Or even for (iterarable): |i: SomeRandomType|?

3 Likes

I would be fine with just being able to specify an integer type or maybe even floats, iterables would probably require another Allocator like interface I think at that point while (it.next()) |next| is clearer.

Another option might be that a loop variable is narrowed to the smallest type possibe if the compiler can figure it out at compile time, e.g.:

For the loop for (0..16) |i| { ... } i would be u4, which would automatically be widened to an usize if i is used as an array index.

Your example of having two u16 start and end variables would also result in an u16, because there’s no way how iterating between two u16 values could result in a wider type than u16.

Only if the compiler can’t figure out the loop type from the loop inputs it would fallback to usize (but then, what if the loop type needs to be wider than usize for some reason, like iterating between two u128 values?).

PS: this “narrow the result type to the smallest possible type an expression allows” would be nice in a lot of other places too (I think there’s a proposal for that, don’t know if it ended up on the chopping block though)

2 Likes

There is a (not yet accepted) proposal for ranged integer types (#3806) that might solve this issue according to this comment by Andrew :slight_smile:

2 Likes

This is precisely the proposal rejected by Andrew without further explanations.

My problem with while is that it’s too verbose. I frequently need to iterate over 3 dimensional spaces like this:

var x: i32 = 0;
while(x < chunkSize) : (x += 1) {
	var y: i32 = 0;
	while(x < chunkSize) : (y += 1) {
		var z: i32 = 0;
		while(z < chunkSize) : (z += 1) {
			the actual code...
		}
	}
}

It just adds so much noise to the code, and I also frequently make mistakes when writing this (did you spot the mistake I added in the example above?), compare that with the regular for loop:

for(0..chunkSize) |x: i32| {
	for(0..chunkSize) |y: i32| {
		for(0..chunkSize) |z: i32| {
			the actual code...
		}
	}
}
5 Likes

It’s also similar to this though :wink:

for was exclusively for iterating over slices until multi-object for loops were added in 0.11. Before that, there was no range syntax and no way to iterate over ranges, you could only iterate over a single slice and optionally capture the index as a second argument, e.g. for (slice) |x, i|. The range syntax was chosen as an alternative to keeping the optional index capture, because there was a concern that the optional capture could result in bugs from programmer mistakes (like for (apples, bananas, oops) |a, b, i| capturing an element of oops and not the index). Being able to iterate over a range of sequential indices is just an nice bonus, but not the main motivation for the syntax. It will probably never be extended to support negative endpoints or increments other than 1. The most likely improvement is that the capture might be narrowed to @Int(n, m) when ranged integers are implemented.

2 Likes

I’m not sure what your pollution concerns are. If it’s that much of a problem, then place the loop in a block.

1 Like