In C++23 with zip it looks something like:<p><pre><code> for (const auto& [x, y] = std::views::zip(a, b)) {
/* ... */
}
</code></pre>
A notable difference is that the ranges don't have to match in size, the loop will run until the end of the shorter range is reached.<p>If it is required for optimization to not check for reaching the end of one of the ranges then it can be achieved with something like:<p><pre><code> for (const auto& [x, y] = std::views::zip(a, std::ranges::subrange(std::ranges::begin(b), std::unreachable_sentinel))) {
/*...*/
}
</code></pre>
I guess it's hard enough to bump into this by accident.
This kind of syntactic sugar used to appeal to me, but now I think it's a pretty weird feature to add to a language. Using zip / enumerate primitives feels a lot more flexible.
Is there other rationale behind this `for` and `while` syntax?
Why not:<p><pre><code> for x in elms {}
for x in elms, i in 0.. {}
</code></pre>
In my view, this seems simpler to read and understand.
In particular, the iteration variable is close to the iterated structure.
In the Zig proposal you have to think about the position of the iterated structure and the position of the iteration variable.<p><pre><code> for (elms, 0..) |x, i| {}
^^^ ^
Ok it is in second position
</code></pre>
I still don't get why `||`. Is this a lambda?
Can I write something like:<p><pre><code> fn func(x: i8, i: i8) void {}
for (elms, 0..) func
</code></pre>
In the same vein I do not understand the rationale behind the `while` syntax. Why `:`?<p><pre><code> while (condition) : (i += 1) {</code></pre>
I've been curious about Zig. I find its cross compilation story using zig cc interesting. I like its focus on simplicity instead of debugging your knowledge of the language. On the surface it looks like a better C, that isn't as complicated as Rust.<p>I'll admit though the syntax is a little off putting. But that is a minor complaint. I know it's not 1.0 and there are still lots to do, but I'm curious if they do more for memory safety. With companies trying to avoid starting new code in memory unsafe languages if they don't have to , I wonder if that will hurt Zig adoption. Right now it just seems like their approach is, reduce Undefined Behavior as much as possible, make the "correct" way of programming the easiest, and have array bounds on by default. Will this be enough to make the language "memory safe" enough?
While this solves one of the issues I've had with Zig, it doesn't seem very flexible. I would love to the same thing for (tuples of) variable-length arrays, arrays of different lengths etc. Now my implementation will still look completely different in slightly different situations.<p>Yes, a flexible solution (a zip function) would probably need iterators but why would introducing them be such a big problem? (FWIW, I know that one can emulate looping over iterators with while() and optionals[0] but it feels a bit dirty.)<p>More generally, my biggest gripe with Zig has been a lack of expressiveness: Things like deep equality checks between structs, arrays and optionals; looping over different kinds of containers; … should just work™. Sure, I understand that Zig doesn't want hidden control flow but OTOH explicit control flow everywhere often just gets in the way of readability and of implementing business logic. I usually follow the approach "Implement first, optimize later" but with Zig the implementation will look completely different depending on which optimization or data structure I choose, so I need to think about optimizations from the start if I don't want to rewrite everything later. I should mention, though, I'm very used to Python these days and haven't written C or C++ in ages, so maybe that sort of culture shock is somewhat expected.<p>Anyway, I'm still excited about the language and my impression is that Andrew Kelley is very open to new suggestions and new ideas, so things will certainly still change in one way or another till v1.0.<p>[0]: <a href="https://news.ycombinator.com/item?id=34958051" rel="nofollow">https://news.ycombinator.com/item?id=34958051</a>
> Ranges can only exist as an argument to a for loop. This means that you can’t store them in variables<p>I am confident this is a mistake. Every time you make a new kind of "thing" in your language somebody will want to do all the same stuff with it that they did with the other things, such as integers, ie in this case store a range in a variable. Ideally you'd just always be able to do that, see Lisp, but it can get very unwieldy, thus this is a reason to avoid making new kinds of thing so the issue doesn't arise.<p>C++ chooses to actually do the heavy lifting here, which is why std::format (and its inspiration fmt::format) was such an enormous undertaking -- C++ can express the idea of a function which takes a variable number of arguments and yet all those arguments are independently type checked at compile time, not via compiler magic but just as a normal feature of the language. This is an enormous labour, and because they don't have any way to fix syntax issues the resulting problem accumulate forever in their language so I cannot recommend it as a course to other languages. It's like the Pyramids, do not build giant stone tombs for your leaders, this is a bad idea and your society should not copy it - however, the ancient Egyptians already did build giant stone tombs and they're pretty awesome to look at.<p>Anyway, Rust chose to make its half-open range type std::ops::Range an actual type which you can store in a variable, pass to functions, modify etc. as well as using it in a for loop. Obviously don't copy Rust here exactly, for one thing Range should probably be IntoIterator, not an Iterator itself if they had it over, but you will wish this was an ordinary type in your language, so, just do it now.<p><pre><code> let a = 0..4; // The Range starting at zero and (non-inclusively) ending at four.</code></pre>
Ranges for for loops was a long time coming. Some C programmers just completely lost their shit at having to use a while loop for some reason. It was such a common complaint that we invented stuff like the following as a joke, that of course became something people actually used because internet gonna internet:<p><pre><code> for(@as([10]void, undefined)) |_, idx| {
_ = idx;
}
</code></pre>
Which, for those unfamiliar with Zig, has `for` iterate over an 'array' of 0-bit values and capture the index while throwing out the value (which would always be 0 because that's the only value a 0-bit type can represent).<p>The implementation of this proposal brings with it the additional benefit of making index capturing less mysterious.
Regarding the lengths must match:<p>> (i.e. you will get a panic in safe release modes)<p>Should I take that to mean there is an unsafe release mode without the bounds check? But UB is mentioned too. Is there UB!?<p>It's 2023; I think we can afford a single branch to avoid UB, even in release mode.
I was fond of the Common Lisp loop macro that handled iterating over multiple things quite nicely:<p><a href="https://lispcookbook.github.io/cl-cookbook/iteration.html#loop-1" rel="nofollow">https://lispcookbook.github.io/cl-cookbook/iteration.html#lo...</a><p>Edit: 27 years since I was paid to write Lisp....
Is must say this looks pleasant, coming from Kotlin. Also ranges seem to work very similarly.<p>Not sure how I fell about the UB - is it really necessary to optimise away a single length check per loop (not iteration)?
Special-casing (same-length) zip and iteration+count might make sense for an imperative language which doesn’t want to go down the rabbit hole of implementing efficient, lazy iterators. It doesn’t make sense in a language where you want the flexibility of switching between (as in: compiling to) serial loops and paralell code, but it makes sense for a language which leans more towards what-you-see-is-what-you-get rather than sufficiently-smart-compiler.
Here is Awk with C99 preprocessing: cppawk!<p>Loop macro for parallel/nested iteration featuring a (user-extensible!) vocabulary of clauses:<p><pre><code> $ cppawk '
#include <cons.h>
#include <iter.h>
BEGIN {
loop (list(iter0, item, list("alpha", "charlie", "bravo")),
list(iter1, ltr, list("a", "b", "c")),
range(i, 1, 3))
{
print item, ltr, i
}
}'
alpha a 1
charlie b 2
bravo c 3
</code></pre>
This is a tiny shell script plus a collection of header files in a small directory structure. It requires an Awk such as GNU Awk, and the GNU C preprocessor.<p>Preprocessed programs cam be captured, to run on systems that don't have the cppawk script or a preprocessor, and with less startup overhead.<p><a href="https://www.kylheku.com/cgit/cppawk/about/" rel="nofollow">https://www.kylheku.com/cgit/cppawk/about/</a>
> <i>The new multi-sequence syntax allows you to loop over two or more arrays or slices at the same time</i><p>In Haskell, this is called a parallel list comprehension:<p><pre><code> [x+y | x <- xs | y <- ys]
</code></pre>
In a normal list comprehension, you have a single pipe, in a parallel one you have as many pipes as how many lists you are zipping. <a href="https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/parallel_list_comprehensions.html" rel="nofollow">https://downloads.haskell.org/ghc/latest/docs/users_guide/ex...</a>
Question for Zig experts:<p><pre><code> for (elems) |x| {
std.debug.print("{} ", .{x});
}
</code></pre>
Why is the .{x} necessary here? What happens if I just write "x"?
```
for (monster_elem_types, monster_hps) |et, <i>hp| {
if (et == .fire) hp.</i> +|= 1; // saturating addition
}
```<p>Anyone knows what language Zig was inspired by(if any)? This doesn't look intuitive nor does it resemble any common syntax.
This is a gripe I have about Go -- a very minor gripe, to be sure, but it's still there. If you want to iterate over two arrays/slices that have the same length, you have to choose between:<p><pre><code> for i := 0; i < n; i++ {
fn(foo[i], bar[i])
}
for i := range foo {
fn(foo[i], bar[i])
}
for i := range bar {
fn(foo[i], bar[i])
}
for i, x := range foo {
fn(x, bar[i])
}
for i, y := range bar {
fn(foo[i], y)
}
</code></pre>
But none of these are satisfactory; what I <i>really</i> want to write is:<p><pre><code> for _, (x, y) := range (foo, bar) {
fn(x, y)
}</code></pre>
coming from total unawareness of zig: in the for (1..5) construct, these integer ranges consistently not including the upper limit element when lists do include the last element, seems surprising. i guess it's a range boundary (1 TO 5), not a list (1 THROUGH 5), but the other behavior feels like a list, so it feels like 5 should be in.
>In the multi-sequence for loop version it’s only necessary to test once at the beginning of the loop that the two arrays have equal size, instead of having 2 assertions run every loop iteration.<p>So, I'm assuming zig generally cannot be used with multi-threaded code? Can the underlying arrays not be modified during the whole loop execution?
Off topic, but I was weighing up trying Zig last night for a project.<p>No doubt Zig has changed alot and is better than it was only a year or two ago.<p>Is anyone here willing to say if they have experienced success and satisfaction using Zig? I'm wanting to do some C library interfacing.
I have absolutely no problem with the good old C-style for loop syntax.<p>I think that a separate foreach or for-each loop, made for built-in or extended containers could be a nice addition.<p>Not seeing much value there.
I'm not sure zip is used enough to add it to the language but since they are also using it for tracking the index maybe that is the primary use case.
A quick syntax question:<p><pre><code> std.debug.print("{} ", .{n});
</code></pre>
What is the '.' before '{n}' for?<p>Thanks!
Sorry, but using new syntax to accomplish something other languages have as library code is not clever.<p>When reading zig code you have to stop and think, "wait does this syntax mean zip or direct product?" But when expressed as a <i>function called zip</i>, the meaning is clear.<p>(Obligatory reminder that zig devs think that sometimes running code inside `if (false)` is a minor bug of no consequence, and after all what are the <i>real</i> motives of anyone pointing it out, eh?)