In Zig, if and while are closely related (if can be thought of as a special-cased while that can only do one iteration) so I’m going to use while for my examples. Consider the case of functions with side effects that return ?T (iterators):
while (it_a.next(), it_b.next()) |a, b| {
doSomething(a, b);
}
If it_a.next() returns null, should it_b.next() be evaluated? Most of the time evaluating the second expression would be pointless and a waste, but in some cases you might want to evaluate all inputs because the side effects are important.
Zig’s design language is very consistent about using keywords for control flow (if, try, and, etc.) so it would be odd for evaluation to stop at the ,. for with multiple inputs doesn’t really have this problem with pointless evalutions because for also inserts a safety check that all inputs are the same length and thus needs to evaluate all argument even if the first one has a length of zero. You might want to read the proposal that led to multi-object for getting implemented since multi-object if/while were also discussed briefly.
The user space functions suggested in replies above will eagerly evaluate all expressions. Lazy evaluation can’t be implemented as a user space function because you can’t capture expressions without evaluating them, so you need to be explicit in code.
If you don’t need an else I would go with @ScottRedig’s suggestion. If you do need some kind of else (and/or a while continue expressions) I would suggest something like this:
while (x: {
break :x .{
it_a.next() orelse break :x null,
it_b.next() orelse break :x null,
};
}) |ab| : ({
const a, const b = ab;
std.debug.print("in continue expression: {d} {s}\n", .{ a, b });
}) {
const a, const b = ab;
std.debug.print("in then expression: {d} {s}\n", .{ a, b });
} else {
std.debug.print("in else expression\n", .{});
}
It’s verbose, but the control flow is very explicit and it can also be extended to support capturing error unions (replace orelse break :x null with catch |err| break :x err). If you’re bothered by the destructuring you might in some cases be able to instead use multiple levels of labeled blocks, but the thing I prefer about this form is that it’s not as prone to simple mistakes like forgetting a break :outer at the end of the inner “then” block, causing it to fall through to the “else” part.