back
21 comments
I have great hope for Zig. I agree with Andrew Kelley on most of the design decisions I've paid attention to.

>for loop can only be used to iterate over elements of an array, if you want to just execute a block N times, you need while…

The Zig docs suggest something like this:

    var i: usize = 0;
    while (i < 10) {
        i += 1;
    }
Is there a plan to provide a mechanism to stop "i" from remaining in scope after the end of the loop? I couldn't find anything relevant with a quick web search.

I use integer iterating for-loops a lot in C. I just checked a 5000 line project of mine that deals with bitmaps. There are 100 occurrences of something like:

    for (int i = 0; i < 10; i++) {
I think these would all become while-loops in Zig. Exposing 100 variables to a larger scope than they need to be sounds like a source of bugs to me.
I'm new to Zig but I think this is how you would do it:

    {
        var i: usize = 0;
        while (i < 10) : (i += 1) {
            // use i
        }
    }
Looks like a solution, a bit annoying to introduce an extra block.
Yeah. And it fails the "make the easy way the right way" test of language design.
I give a c-like for loop (or while loop) a 40% odds of making it into the language. That number being pulled out of my nether regions.
Everyone thinks this is a problem because they are used to being allowed to shadow variables, and they've been bitten by variable shadowing, and they solved the problem by limiting the scope of the variable so as to reduce the likelihood of shadowing. But you can't make any mistake in Zig. If you tried to declare `i` later in the scope, you would get "error: redeclaration of `i`", which you could then solve with wrapping the mutable state in a scope. With no variable shadowing allowed, having many locals in scope is not a problem.
As a hack, you can use an array of u0 to make simple loops:

    for ([1]u0{0} ** 10) |_, i| {
        std.debug.print("{d}", .{i});
    }
Not a Zig programmer, but that looks horrible.
Yeah, it's a hack.

If you're doing it a lot you can abstract it into a function to clean it up:

    fn count(comptime n: usize) [n]u0 {
        return [1]u0{0} ** n;
    }

    for (count(10)) |_, i| {
        std.debug.print("{d}", .{i});
    }
or, if you don't need to capture the index:

    for (count(9)) |_| {
        //I can do it 9 times
    }

Personally I've found I rarely need this kind of loop, so I just use the 'while' formulation because it is more obvious.
Hah! i thought it was not a hack so much as a 'joke' that someone had discovered and posted to the discord, to much amusement. Are people actually using this?

I will be amused when std.math.range shows up with this implementation.

Is there a performance penalty for this?
There is of course

  { var i: usize = 0; while (i != 10) : (i += 1) {
    ...
  }}
I.e., you are not obliged to plaster on a newline after every semicolon. That is roughly as busy as the equivalent C++ loop,

  for (auto i = 0u; i != 10; ++i) {
    ...
  }
but syntax is syntax. You would be much better off with a function that takes a lambda, and pass the body as that lambda. In C++, a loop using that would look like

  forloop(10u, [&](auto i) {
    ...
  });
That's not a much shorter than the built-in, but leaves a lot less room for mistakes; i.e., if there was a typo in the built-in, it might be very hard to make yourself see it even knowing it must be there.

I can't find anything to suggest Zig has lambdas. That's a planet-sized hole, nowadays.

(BTW, forloop in C++ is just

  template <typename Ix, typename Body>
  void forloop(Ix top, Body&& body) {
    for (Ix i{}; i != top; ++i) body(i);
  }
In use it compiles to instructions identical to the built-in loop.)
> I can't find anything to suggest Zig has lambdas.

Closures are problematic in low-level languages, as their capture behaviour requires tricky syntax and/or implicitness, and Zig is very much opposed to both. Non-capturing anonymous functions expressions will be added (https://github.com/ziglang/zig/issues/1717), while a limited form of closure is being considered (https://github.com/ziglang/zig/issues/6965).

Of all the syntaxes I've seen to do this, I like Kotlin's best:

    repeat(10) {
        println("Counter is $it")
    }
Kotlin optimizes nicely for the most default case while giving you flexibility if you need it (different parameter name, step, etc...).
That looks way better than the while or for loop you usually see. Maybe this could be done in Zig as a comptime function?
Also note that this is not a special syntax, it's a library function called `repeat()` which leverages Kotlin's support for out-ouf-parens closures.
Even if we assume that this is a big problem, identifying it is the easy part, the harder part is coming up with a solution.

If you just think about this specific issue, then it seems obvious that the solution should be to change the syntax to accommodate what other C-like languages are doing, but if you look at the language as a whole will see all the other problems that get introduced, making this change less straight forward than it might seem initially.

My recommendation to the people that want to see this changed is to do the work to understand enough about Zig to be able to produce a proposal and see how that goes. There even are ~regular meetings where a guest can participate to present a proposal to the language design committee.

That said, here's my take (just to clarify, I'm not part of the language design committee): I think it's fine as is and the solution is to just use blocks the right way.

In other comments people showed the example of encapsulating a while loop in a block, but that's just an artificial example that I agree doesn't seem too convincing. The reality is that leaking a variable is not terribly bad per se, it only becomes bad when a function has a lot of things going on and also maybe multiple loops, like you mentioned here:

> Exposing 100 variables to a larger scope than they need to be sounds like a source of bugs to me.

My argument at this point is that if you have a function with multiple while loops and enough things going on that leaking a variable would be a problem, then you should have blocks in place already, which in other words means that it's not the while loop that needs to be artificially wrapped in a block to overcome a language limitation, but rather that the loop + its surrounding context should be wrapped in a very natural block regardless.

Of course having the ability to use a while loop to restrict the lifetime of its index would be better, but what I mentioned just now about wrapping each context makes me think that it's not that bad of a problem and, on the other hand, the design problem I mentioned in the beginning makes me think, given what I know currently, that this might very well be a sweetspot despite the "common sense" complaints that people have.

> feel like compiler could use some more strict warnings, too

Warnings in Zig are called 'compiler errors'. Which is to say, Zig doesn't do warnings. If the compiler thinks there is a problem, you must fix it. Usually you've made a mistake, but sometimes you just need to be explicit about what you want to happen. In this particular case I believe there is an issue raised about it, but I couldn't find it on github.

> Zig float literals have type comptime_float, which is essentially the largest type, ie. f128. To use a different type you have to cast it first and I’m not loving the syntax: [...]

> It is also a bit inconsistent, as some functions (like pow) will take a type parameter and allow for comptime params: [...]

Yes, this is an inconsistency in the standard library that hasn't yet been addressed as Zig currently doesn't place a high priority on polishing the standard library.

> taking it for what it was – a modern “C+” alternative

Andrew Kelley, Zig's designer, presents Zig as an alternative to C because it can replace C where it is used, but even though C served as a conceptual starting point in the language's design process, comparing the language itself to C (or "C+"), as opposed to C++, doesn't quite capture the language's design and feel.

Superficially, what makes Zig feel "slim" compared to C++ is that 1. it disallows any kind of name overloading (and, so, in particular operator overloading) -- any (namespaced) name in a Zig program can refer to only one definition -- and 2. it is a very simple language that can be fully picked up in a day or two by anyone with experience in any low-level language (C, C++, Rust, or Ada). But Zig is both safer and more expressive than C++; any "rich" C++ construct that makes clever use of, say, templates, can be expressed in Zig and more easily, thanks to Zig's comptime and introspection, and much more. It is no coincidence that programs are short in comparison to other low-level languages. It's not that the language is terse, but that its simple constructs are powerful, similar to how Lisps can be simple yet expressive, although Zig's comptime + introspection replaces Lisp's macros as the "power feature" (and comptime + introspection is easier to read and write than macros). So while the language shares some basic primitives with all low-level languages, like precise control over data representation, it doesn't feel like any other language.

I see Zig as an entirely new, and quite revolutionary approach to low-level programming, that doesn't follow any design tradition in that space, neither C's sparseness nor C++/Ada/Rust's linguistic richness. It is something new altogether.

Different languages appeal to different people based on their aesthetic preferences and priorities (e.g. C++ and Rust prioritise the code appearing as if written in a high-level language when read on the page, while Zig prioritises explicitness and compilation times), and surely some developers would prefer C++ or Rust to Zig, just as surely as others would prefer Zig, but to understand what Zig is, it has to be experienced, because it really is hard to compare to anything else. Thankfully, being so simple and easy to learn, experiencing it requires very little investment. You may find it a promising prospect for any low-level programming task, regardless of whether it would have otherwise suited C or C++. It can replace C++ just as much as C, and even C++ developers would find it compares very favourably when it comes to expressive power.

That 99 line cpp path tracer linked in the post isn't really 99 lines. It's minified C++. If expanded to a more readable version it will probably be around ~250 lines. Still not too big but definitely not 99.