For-loop counter other than usize

Hello.

As far as I know, the loop counter of for-loop using a range syntax is always usize:

for (0..10) |i| {} // `i` is `usize`

However, it is really annoying to downcast the counter to pass it to functions that take other types such as u32.

Is there any easy ways to make the counter other types?

1 Like

Hello @smallkirby

There is no way to declare the range type.
See why: Why are range variables always usize? - #2 by IntegratedQuantum

There are two ways to loop: for and while.

  • Cast to the desired type:

    for (0..10) |_i| {
      const i = @as(u32, @intCast(_i));
    }
    
  • Declare the desired type:

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

EDIT: added @intCast

2 Likes

I came up with something similar

const std = @import("std");

pub fn main() !void {

{
    var counter:u8=10;
    while (counter!=0) : (counter-=1) {
        std.debug.print("All your coding are belong to us in {d}; Which happens to be a {}\n", .{counter,@TypeOf(counter) });
    } // end of while
}

{
    const counter=[_]u8{1,2,3,4,5,6,7,8,9};
    for (counter) |item|{
        std.debug.print("All your coding are belong to us in {d}; Which happens to be a {}\n", .{item,@TypeOf(item) });
    }
}

{
    for (0..10) |index|{
        const index2:u8=@intCast(index);
        std.debug.print("All your coding are belong to us in {d}; Which happens to be a {}\n", .{index2,@TypeOf(index2) });
    }
}

}


Hi

This didn’t work for me I got

main_test.zig:7:28: error: expected type 'u32', found 'usize'
        const i = @as(u32, _i);
                           ^~
main_test.zig:7:28: note: unsigned 32-bit int cannot represent all possible unsigned 64-bit values

Perhaps I mistyped it? Here is the full code.

const std = @import("std");

pub fn main() !void {

{
    for (0..10) |_i| {
        const i = @as(u32, _i);
        std.debug.print("All your coding are belong to us in {d}; Which happens to be a {}\n", .{i,@TypeOf(i) });
    }
}

}

Anyway I got round this issue with

const std = @import("std");

pub fn main() !void {


//{
//    for (0..10) |_i| {
//        const i = @as(u32, _i);
//        std.debug.print("All your coding are belong to us in {d}; Which happens to be a {}\n", .{i,@TypeOf(i) });
//    }
//}

{
for (0..10) |_i| {
  const i = @as(u32, @truncate(_i) );
  std.debug.print("All your coding are belong to us in {d}; Which happens to be a {}\n", .{i,@TypeOf(i) });
}
}

}

1 Like

I think what you’re looking for (in general) is @intCast: Documentation - The Zig Programming Language

Truncation is best used when you really intend to truncate… I can imagine that you may end up with weird results if you’re not careful with truncation.

I can see I was being evil. In my defense I was only trying to get the code working.
My MO is to get it compiled and working, then clean up the mess afterwards.

My first post example did as you suggested.

{
    for (0..10) |index|{
        const index2:u8=@intCast(index);
        std.debug.print("All your coding are belong to us in {d}; Which happens to be a {}\n", .{index2,@TypeOf(index2) });
    }
}

It’s easy to forget all the casting rules/operators. s’all good ¯\_(ツ)_/¯

Thanks everyone.
I understood that there’s no way to allow non-usize counter for a loop counter. So I’m going to use a downcast (truncate) or while loop instead. Thank you for your advices :slight_smile:

Give me a little bit longer almost got a solution for which you might be happy with and isn’t evil.
Just got to put the guard rails in and write the tests.

Here is what I came up with … It’s a bit clunky. I am sure I can refine it.
Please feel free to comment and improve.

It accepts u8,u16 or u32

Example

In the below example it counts to 0…3 in u8 using comptime.

    for ( countTo(@as(u8,3)) ) |item| {}

Output

zig run main_ziggit.zig
(Evil laugh) All your coding are belong to us in 0; Which happens to be a u8
(Evil laugh) All your coding are belong to us in 1; Which happens to be a u8
(Evil laugh) All your coding are belong to us in 2; Which happens to be a u8
sgeorge@debian:~/zig_bits/play/20240616_ziggig_forloop/src$ zig test main_ziggit.zig
All 1 tests passed.

Full code

const std = @import("std");

pub fn main() void {

    // To use this; change u8 to your desired type ie u8,u16,u32. Then 10 your desired count up to maybe 3
    // this works in comptime, so the I don't think the long arrays should be a problem
    for ( countTo(@as(u8,3)) ) |item| {
        std.debug.print("(Evil laugh) All your coding are belong to us in {d}; Which happens to be a {}\n", .{item,@TypeOf(item) });
    } // end of for

//    // Lets do some quick tests
//    for ( countTo(@as(u16,3)) ) |item| {
//        std.debug.print("(Evil laugh) All your coding are belong to us in {d}; Which happens to be a {}\n", .{item,@TypeOf(item) });
//    } // end of for
//
//
//    for ( countTo(@as(u32,3)) ) |item| {
//        std.debug.print("(Evil laugh) All your coding are belong to us in {d}; Which happens to be a {}\n", .{item,@TypeOf(item) });
//    } // end of for
//

} // end of main

pub fn countTo(comptime top:anytype) [top]@TypeOf(top)
{
    comptime std.debug.assert(@TypeOf(top) == u8 or @TypeOf(top) == u16 or @TypeOf(top) == u32);
    var return_array:[top]@TypeOf(top)=undefined;
    for (0..top) |item| { return_array[item]=@intCast(item); }
    return return_array;
} // end of fn

test "countTo"
{
    try std.testing.expect( countTo(@as(u8,3))[0] ==  0);
    try std.testing.expect( countTo(@as(u8,3))[1] ==  1);
    try std.testing.expect( countTo(@as(u8,3))[2] ==  2);

    try std.testing.expect( countTo(@as(u16,3))[0] ==  0);
    try std.testing.expect( countTo(@as(u16,3))[1] ==  1);
    try std.testing.expect( countTo(@as(u16,3))[2] ==  2);

    try std.testing.expect( countTo(@as(u32,3))[0] ==  0);
    try std.testing.expect( countTo(@as(u32,3))[1] ==  1);
    try std.testing.expect( countTo(@as(u32,3))[2] ==  2);

}

Hi, @endlessly_amused .
I really appreciate your answer, but I feel that typing countTo(@as(u8, 3)) is not so much simpler or easier than:

for (0..3) |_i| {
    const i: u8 = @truncate(_i);
} 

Go with what you feel comfortable with. And thanks for the feed back.

However, having said that, it was mentioned that @truncate was probably not a good idea.
It just chops off the leading zeros. You should probably use @intCast() instead.

There were two versions.

My version was

    for (0..3) |_index|{
        const index:u8=@intCast(_index);
        std.debug.print("Count {d}; in {}\n", .{index,@TypeOf(index) });
    }

The winning entry and therefore better entry (from dimdin) was

for (0..10) |_i| {
  const i = @as(u32, @intCast(_i));
}

All the best

1 Like

For anyone who is interested, here is my final piece of code to answer the question.

Q) A For-loop, with a range syntax of (0…3), will use a usize type. Can we make it u23?

A) This code will count to 3 in u23, using comptime.

    const top:u23=3; 
    for (@"0.."(top)) |item| {
        std.debug.print("1 Count {d}; in {}\n", .{item,@TypeOf(item) });
    }

Full code

const std = @import("std");

pub fn main() !void {

    const top:u23=3; // will count 0..3 all in u23
    for (@"0.."(top)) |item| {
        std.debug.print("1 Count {d}; in {}\n", .{item,@TypeOf(item) });
    }

}

pub fn @"0.."(comptime top:anytype) [top]@TypeOf(top)
{
    switch (@typeInfo(@TypeOf(top))) {
        .Int,.ComptimeInt => {
            comptime std.debug.assert(top >= 1);
            var return_array:[top]@TypeOf(top)=undefined;
            for (0..top) |item| { return_array[item]=@intCast(item); }
            return return_array;
        },
       else => {
            std.debug.print("!! You need to supply an int to @\"0..\"()\n", .{} );
            unreachable;
       },
    }
}

2 Likes

while it’s cool that you can do such stuff in Zig, …I would still go with the while loop ^^

btw. this question was (one of the rare zig questions) on StackOverflow recently; Zig for loop range iterator with custom int type - Stack Overflow

1 Like

This is clever, but please carefully consider the cost of doing this.

This can cause calls to memset and increases the workload. This is also particularly rough if you try to count to anything large as it can also consume a lot of stack memory.

Make sure to always look at the results of the assembly when doing something like this because it may not be doing what you think it is.

2 Likes
2 Likes

I’d like it if the language supported something like this:

for (0..) |byte: u8| {
    // ....
}

There are a few possible ways to phrase this, two more: for(@as(u8, 0..)), for(@as(u8, 0)..), I just chose my favorite from the associated proposal. The other two turn @as into “magic”, which, a builtin can be magic, but that tendency should be resisted. The above syntax is consistent with specifying the type of a numeric literal to cast it down from comptime_int.

That issue was closed, this was the main part of the reasoning given (quote is Andrew Kelley):

If you tried to implement your proposal, you would see the problems with it.

That strikes me as a good reason for Andrew / the Zig Foundation to not work on the issue, but not so great a reason to close it. There’s a difference between “this is not good for Zig” and “this is harder than you think, and the benefits are not worth the time I would have to devote to it”. Either of these is a fully legitimate reason for the core team not to devote time to the feature, but if the latter reasoning is in fact primary, maybe someone else would be willing to tackle the implementation complexity without making it the team’s problem.

There’s also a question of where to stop, but “for ranges are increment only, integer only, and always increment by one, automatically” is defensible. Anything else should be using a while loop, whether with a postcondition or an iterator. That keeps the semantics of for loops clean, and consistent with their use to iterate over slices/arrays/Vectors, but also opens up range types which aren’t usize, which is clearly valuable (or people wouldn’t keep asking this question).

It seems to have been closed as part of a general cleanup (see the rest of the comment). That kind of thing happens. I do hope room is left to reconsider the question at some later time.

4 Likes

I’d like to offer a slightly different perspective: You should use the right tool for the right job.

My understanding is that the for loop is intended for forward-iterating over array elements, which are indexed by usize, one element at a time. It is not intended for general “count from N to M” tasks. Prior to the addition of multi-object for loops in Zig 0.11.0 you couldn’t even iterate over ranges, only an array and optionally its indices (equivalent to what is now for (slice, 0..) |a, i|).

Refer back to the “communicate intent precisely” bulletpoint of zig zen. If your for loop iterates over a range like 0..10 and you are not using the range variable for indexed access, you are likely misusing the for loop construct and should consider instead using the general-purpose while loop for clarity, to avoid giving a reader the false impression that you are iterating over the elements of some array.

4 Likes

This is working backward from what is, to what should be.

Also, peer type resolution takes care of indexing by smaller values:

test "index with a u8" {
    const nums = [_]u64{ 23, 34, 56, 78 };
    const idx: u8 = 1;
    std.debug.print("index with u8: {d}\n", .{nums[idx]});
}

As it should.

Debatable:

test "count from N to M" {
    const n = 5;
    const m = 10;
    for (n..m) |i| {
        std.debug.print("index is {d}\n", .{i});
    }
}

Sure looks to me like you can use it for, checking my notes here, “general count from N to M tasks”.

Says you!

1 Like

I have seen this argument before. In my mind, it would apply to an hypothetical foreach within zig: the name would make it extremely clear you are iterating over each and every element (of an array).

I liked @dude_the_builder’s Range construction – if only that could be used with a for loop… One can only dream.