How to assign optional tag

Optional types are isomorphic to this union:

const Optional = union(enum) {
    missing,
    present: T,
};

When assigning to such a union, Zig lets the programmer choose to assign the tag, the value, or both at once. Unions support all possible combinations, but optionals have one case I can’t figure out:

Optional(T) ?T
set just tag to null x = .missing x = null
set just tag to non-null x = .{ .present = undefined } -
set just non-null value x.present = value x.? = value
set non-null tag and value x = .{ .present = value } x = value

What’s the missing syntax in the chart?

x = undefined

Welcome to ziggit :slight_smile:

1 Like

Not exactly. Optionals of certain types, particularly pointers, are optimized to use special values for null instead of a tag. Pointers can simply be set to 0, like in C. I think this might be the root of your confusion.

2 Likes

I don’t think so. You cant guarantee that x != null in this case, since x could be anything. Of course, in debug mode undefined gets written to a set value, but according to the definition of undefined, this is not exactly correct.

As I understand it, you can’t set an optional value to “not null” without just giving it a value. If you don’t care about the value just use some sensible zero value.

1 Like

You are right.

In practice it works for Debug and ReleaseSafe (where undefined sets the value with 0xaa) but does not work for ReleaseSmall or ReleaseFast (it is null).

Example code:

const std = @import("std");

pub fn main() void {
    const T = u32;
    var foo: ?T = undefined;
    if (foo) |v| {
        std.debug.print("have foo={x}", .{v});
    } else {
        std.debug.print("foo=null", .{});
    }
    foo = null;
}
1 Like

It’s missing on purpose. Setting the tag should come with a meaningful value, otherwise it loses the semantics of an optional type.

6 Likes

@dimdin

You’re triggering undefined behavior with that code, branching on a value of undefined. In my case I’m trying to write a well-defined program.

@LucasSantos91

I wouldn’t say so. It’s a common pattern in Zig to get a pointer to uninitialized data and assign to it later (see Allocator.create(), etc). In fact, the langref explicitly acknowledges the usefulness of this pattern—it’s under “To change the active field of a union when a meaningful value for the field is not known, use undefined”

2 Likes

What you’re observing is Zig’s lack of true default values. That’s a deliberate design decision. By “true” I mean that the proposition “for all T, there is a default T of a specific state defined by the language”. In Zig, there isn’t.

In the case where T does have a complete default value, you can write it like this:

const a_value: ?T = .{};

The master branch recently added declaration literals, which going forward will be the preferred way to initialize something to a known-good starting state.

But Zig, unlike several other languages, does not have a well-defined default value for a type, unless all the fields are explicitly defined to have default values. So there isn’t an equivalent of what you’re looking for in all cases, only in some.

24 posts were split to a new topic: Options, Enums and Nullability

I think this is the wrong mental model.

For some ?T where every combination of bits interpreted as a value of type T is a valid T, optionals could be thought of as something like struct { is_null: bool, payload: T }. Under this model, you could indeed initialize the is_null tag and the payload separately.

But for some ?T where some combination of bits interpreted as a a value of type T is not a valid T, such as the value 0 for a (non-allowzero) pointer type like *u8, optionals could be thought of something like enum(T) { null, ... }, where the fictional syntax ... enumerates every possible valid value for T. Under this model, there’s no tag, only a special sentinel value; the value is either a valid T and thus not null, or the invalid sentinel value selected to represent null.

4 Likes

Are you saying union(enum)s are not eligible for niche optimization? I’m aware the compiler doesn’t perform this optimization today, but I thought the plan was to fix that.

x = @as(T, undefined)
4 Likes

Now this is actually getting somewhere, thank you! The syntax is clunky but I can live with that.

#22215

This doesn’t seem to work:

pub fn main() void {
    var f: ?*u8 = null;
    f = @as(*u8, undefined);
    std.debug.print("value: {?}\n", .{f});
}

I also tried:

pub fn main() void {
    var f: ?*u8 = @as(*u8, undefined);
    _ = &f;
    std.debug.print("value: {?}\n", .{f});
}
$ zig run -O ReleaseFast opt.zig
value: null

I still don’t believe there is a way to set a non-null value for any ?T without simply setting it to a value of T. I’m also not convinced that it is necessary, I would be grateful if you could provide a use case.

I was wrong, I did not know you could do this. However, .? is still only acting as an assertion, I don’t think it can be used to strictly access “just the value” of foo, because that’s not what it does semantically.

1 Like

So we’ve established something here: This is a bad idea, and always run tests in ReleaseFast mode if trying to figure out what kind of weird stuff you can actually get away with.

All of this passes in both Debug and ReleaseFast mode.

const ExampleStruct = struct {
    v: u64,
};

fn returnOptional(val: u64) ?ExampleStruct {
    var example: ExampleStruct = undefined;
    example.v = val;
    return example;
}

test "It returns optional" {
    const opt = returnOptional(23);
    if (opt) |it| {
        try expectEqual(23, it.v);
    } else {
        try expect(false);
    }
}

fn takesOptional(maybe_it: ?ExampleStruct) !void {
    if (maybe_it) |it| {
        try expectEqual(128, it.v);
    } else {
        try expect(false);
    }
}

test "It passes optional" {
    var example: ExampleStruct = undefined;
    example.v = 128;
    try takesOptional(example);
}

test "Trivial assignment case" {
    var example: ExampleStruct = undefined;
    example.v = 128;
    const maybe_example: ?ExampleStruct = example;
    try takesOptional(maybe_example);
}

I don’t think real code exists where doing things this way would not be strongly preferred. You can always assign a T to a ?T. So if you need to build a T up by parts, you don’t need to create it as a ?T, create it as a T and: return it, pass it, or assign it to a ?T.

3 Likes

I’ve broken off the conversation, as IMO it had veered away from answering the question and into more brainstorming discussion.

Long and Short of it is, As of right now, I don’t know if there is a good, stable, and guaranteed way to do what you are asking to do. Others are free to posit other solutions, or we can select one of the above answers as the “solution”.

2 Likes