Multiple optional captures in a if statement?

So I was writing code in zig and wanted to capture multiple optionals in a if statement and run code if they both not null, but I realized that i will have to do something messy like nesting if statements, I strongly belive that this should be a feature in zig.

I have two syntax in mind

if (opt_a and opt_b) |a, b| {}
if (opt_a, opt_b) |a, b| {}

We shouldn’t reuse keywords/operators for different purposes, besides for(as, bs) |a, b| is already syntax for multiple captures (specific to for loops).

I am not opposed to this, but for a current workaround:

if (opt_a != null and opt_b != null) {
  const a = opt_a.?;
  const b = opt_b.?;
  //...
}
1 Like

I don’t think and makes sense because optionals aren’t booleans or truthy values (because Zig doesn’t have truthy values, also or wouldn’t make any sense), the second syntax, hmm, maybe.

Instead, in a bunch of situations I would write something like this:

const a = opt_a orelse return;
const b = opt_b orelse return;
...

Or return null or even an error based on what function it is, if you don’t want to return you can put it into a block and break instead.

1 Like

Since getting new syntax added to the language is very unlikely, and since this is one of many sorts of conveniences one might want, I suggest doing it with a comptime function. I’ve written one below.

If you don’t like passing T and N you could modify it to use optionals: anytype and use type introspection to derive its type as well as the return type. Personally I don’t think that’s worth the trouble.

pub fn getOptionals(T: type, comptime N: usize, optionals: [N]?T) ?[N]T {
    var values: [N]T = undefined;
    for (optionals, 0..) |opt_val, i| {
        if (opt_val) |val| {
            values[i] = val;
        } else {
            return null;
        }
    }
    return values;
}

test "getOptionals" {
    const x_opt: ?usize = 1;
    const y_opt: ?usize = 2;
    const z_opt: ?usize = 3;

    const x, const y, const z = getOptionals(usize, 3, .{ x_opt, y_opt, z_opt }) orelse
        return error.TestUnexpectedError;

    try std.testing.expectEqual(6, x + y + z);
}

Edit: With more comptime programming (beyond my current level) I believe you could pass a tuple of optionals instead of an array, allowing each value to have a different type, and return an optional tuple of those non-optional types.

5 Likes

Using anytype and returning a tuple would allow using this with more than one optional type. Great minds think alike :slight_smile:

I was going to provide an implementation of that, but I found a compiler segfault instead, lmao.

3 Likes

If you don’t care about else statements, this already fairly simple:

const std = @import("std");

pub fn main() void {
    foo(null, null);
    foo(1, null);
    foo(null, 2);
    foo(1, 2);
}

fn foo(maybe_a: ?usize, maybe_b: ?usize) void {
    if (maybe_a) |a| if (maybe_b) |b| {
        std.debug.print("{d}\n", .{a + b});
    };
}

Otherwise, the orelse with a return or break from a labeled block.

7 Likes
const std = @import("std");

pub fn AllOrNothing(Tuple: type) type {
    var types: []const type = &.{};
    for (std.meta.fields(Tuple)) |f| {
        switch (@typeInfo(f.type)) {
            .optional => |o| types = types ++ [1]type{o.child},
            else => @compileError("Expected an optional but got: " ++ @typeName(f.type)),
        }
    }
    return std.meta.Tuple(types);
}

pub fn allOrNothing(optionals: anytype) ?AllOrNothing(@TypeOf(optionals)) {
    var res: AllOrNothing(@TypeOf(optionals)) = undefined;
    inline for (optionals, 0..) |optional, i| {
        res[i] = optional orelse return null;
    }
    return res;
}

test allOrNothing {
    const x_opt: ?usize = 1;
    const y_opt: ?bool = true;
    const z_opt: ?[]const u8 = "you win";

    const x, const y, const z = allOrNothing(.{ x_opt, y_opt, z_opt }) orelse
        return error.TestUnexpectedError;

    try std.testing.expectEqual(1, x);
    try std.testing.expectEqual(true, y);
    try std.testing.expectEqualStrings("you win", z);
}
2 Likes

I thought this answer from the subreddit was pretty good https://www.reddit.com/r/Zig/s/bnoPrhcGcr

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.

7 Likes

it is a somewhat arbitrary decision, as long as it is consistent between while and potential future if is more important.

I may be wrong, but wouldn’t inline fn solve this issue.

Regardless, I very much prefer and encourage the explicit control flow.

Even with inline fn you can’t define a function like lazyEval(a(b(c())), x(y(z()))) for the general case where absolutely no part of the expression on the right side of the comma is evaluated. You would need something like the rejected stack-capturing macros proposal for that.

1 Like

Thanks, I don’t know why I didn’t think of that. I prefer this because it is so simple.

Thanks for showing how to do the comptime part of this, this will come in handy. It isn’t as complex as I thought it would be.

1 Like