Grammar related suggestions

It would be nice to use the general FOR loop syntax of C/C++. It is common to use WHILE for condition-based iteration. And FOR is used for count-based iteration. Therefore, it would be better to abolish the incremental syntax of the current WHILE loop and add the general syntax of FOR loop.

It would be nice to use the ternary operator syntax of C/C++. The if/else expression is less readable and the syntax is inconsistent in that the statement and expression change depending on the presence of the block.

const std = @import("std");

pub fn main() void {
  for (var i = 0; i < 10; i += 1) {
    std.debug.print("Hello, {d}!\n", .{i + 1});
  }

  // var i = 0;
  // while (i < 10) : (i += 1) {
  //   std.debug.print("Hello, {d}!\n", .{i + 1});
  // }

  var a = 0;
  var b = 0;

  true ? a : b;
  if (true) a else b;

  true ? getPriorityID() : getPriorityID2();
  if (true) getPriorityID() else getPriorityID2();
}
1 Like

Welcome to Ziggit @TinyProbe .

The Zig for loop has evolved into a powerful language feature. Maybe you’ve seen older examples where while was the only option for iterating over a range, but the for loop can do that now:

for (0..10) |i| { ... }

It can also iterate over multiple sequences, allowing for results similar to Python’s zip builtin:

for (slice_a, slice_b) |item_a, item_b| { ... }

As for the ternary operator, I think that the main reason some languages have decided to avoid it is because it has been heavily abused in languages that do have it. For example, you could have large blocks as the expressions, making it difficult to know that they’re actually part of a single ternary op expression.

condition ? block: {
    // many, many lines of code
} : block: {
    // many more lines of code
};

I understand the ternary operator.

But can I use the FOR loop in reverse? It is not provided syntactically, so I am creating a new variable and using it. However, I think it can be provided syntactically in zig. And I think it should be provided.

const std = @import("std");

pub fn main() void {
  for (0 .. 5) |i| {
    std.debug.print("Hello, {d}!\n", .{i + 1});
  }
  // for (5 .. 0) |i| {
  //   std.debug.print("Hello, {d}!\n", .{i + 1});
  // }
  const siz = 5;
  for (0 .. siz) |i| {
    const ii = siz - i - 1;
    std.debug.print("Hello, {d}!\n", .{ii + 1});
  }
}

Plenty of discussion on this sort of thing in the past, see

and all of the issues it links to.

1 Like

It would be best if a C/C++ style FOR statement was introduced, but since the syntax is long I think it is a reasonable choice to add several specialized grammars to compensate for it. However, this issue will be keep discussed in the future, and the form reached after considerable trial and error will be different from the current one. Or the language may not be zig. And even if someone proposes a new FOR syntax in a perfect and flawless form now, it is very likely that it will not be accepted because it is too different from the current one. Therefore, I think the best way is to inherit the C/C++ style syntax, and when the current syntax is sufficiently verified through long-term research in a way that is good to use in the future, I think it is right to introduce a new syntax.

It’s worth considering what Andrew Kelley says in this talk:

It’s my goal and it’s still my goal to not get carried away, like, adding a lot of stuff. There’s a lot of features that are not in Zig and that’s very intentional, and it’s hard because, you know, everyone likes to propose features and it’s really easy to make fun of C++, but everyone has that one feature from C++ that they like and want to put it in in your language, they want to put it in Zig, and if everyone had their favorite feature from C++ in Zig it would just be C++. But I’m not gonna let that happen

(there was a similar sentiment expressed in the Q&A of this older talk as well)

7 Likes

Hi, welcome to the forum. I come from a C/C++ background, and I have to disagree with you Zig’s for loop syntax, in my opinion, is better than the one in C/C++. Why? If we look at the typical use pattern of for loops in those languages, you’re usually iterating over a range of something: from the first character of a string until the '\0', from base_ptr to end_ptr, or over a range like for (size_t i = 0; i < 10; i++). The point is, my use of loops in C/C++ is pretty consistent.

  • For loops: for iterating over a given range of something.
  • While loops: for everything else.

Now, if you look at the syntax of a for loop in C/C++, you need to provide three statements, all optional: an initializer, a condition, and the final statement executed at the end. That’s three potential points of failure, three opportunities to make a mistake.

How many times have you encountered a bug because you didn’t use the right types say, an initializer of type int and a condition of type unsigned? I know I have (though with more experience, it happens less). And how many times have you mistakenly written for (size_t i = 0; i <= 10; i++) instead of for (size_t i = 0; i < 10; i++)? That still happens to me if I’m not careful. I can’t even count how many times my C++ program has crashed because, in an iterator loop, I used ++it instead of it++ I just prefer the prefix increment aesthetic in C.

My point is, the for loop in Zig, combined with the Zig compiler and bounds checking, creates an environment where there’s less room for error. You don’t have to create and initialize the iterator properly; it’s deduced from the range you’re iterating over. There’s no room to mess up the condition because the only condition is the range itself, and the compiler will throw an error if it’s invalid. I understand the argument about not being able to easily iterate backward in Zig, but that fits with the philosophy of making the correct thing easier—loops that decrement have caused me countless issues.

In any case, I get your opinion, and I wouldn’t mind if they changed or improved the for loop. But as it stands, the current one works well for me, and when it doesn’t, the while loop fills the gap which seems very much in line with how these loops are used in C/C++.

I appreciate the individuality of zig, one thing I wish it had was support for reverse FOR statements.

1 Like

Although doing this isn’t too much additonal work:

    const a = [_]u8{ 1, 2, 3, 4 };
    for (1..a.len + 1) |i| {
        std.debug.print("{d}\n", .{a[a.len - i]});
    }

I agree that being able to just do for (a.len - 1..0) |i| {...} would be nice. However, the for loop index is a usize which is convenient because that’s the type used for indexing. But if you can do this:

for (3..-3) |i| {...}

then i would probably have to be an isize which would require casts to use it for indexing. As always, tradeoffs, tradeoffs…

2 Likes

For iterating over slices in reverse I would just want to know that using iterators like this is properly optimized by the compiler, so that I don’t have a reason to write manual loop logic in those cases (I haven’t done any investigation into that, so maybe that is already the case)

var it = std.mem.reverseIterator(slice);
while (it.next()) |item| {
    ...
}

It would be good to know that we can package certain common usage patterns with things like iterators without loosing a bit of performance. Has anyone done a bunch of testing on things like this?

1 Like

it.nextPtr() returns a pointer instead of a value for item, you can use that when you don’t want to pay the penalty for copying all items values.
Otherwise the performance is the same with a standard while backwards iteration.
The returned reverse iterator source code is:

    struct {
        ptr: Pointer, // initialized with the slice ptr
        index: usize, // initialized with the slice len
        pub fn next(self: *@This()) ?Element {
            if (self.index == 0) return null;
            self.index -= 1;
            return self.ptr[self.index];
        }
        pub fn nextPtr(self: *@This()) ?ElementPointer {
            if (self.index == 0) return null;
            self.index -= 1;
            return &self.ptr[self.index];
        }
    };
1 Like