Proposal(std.meta): Is there already another (idiomatic) way to retrieve tagged union active field's value?

Hi, I had a hard time extracting an element from a union without having to use the syntax union_value.active_tag. (It was needed for some generic code involving type-safe indexes referring to a list of unions…)
After some tinkering/experiment, I end-up with this. Planning to propose it as part of std.meta.

fn retrieveFieldFromUnion(
    comptime UnionType: type,
    comptime desired_tag: std.meta.Tag(UnionType),
    union_value: UnionType, // may be a `*const` ?
) !@FieldType(
    UnionType,
    @tagName(desired_tag),
) {
    if (std.meta.activeTag(union_value) != desired_tag)
        return error.cantAccesInactiveTag; // TODO: may just return a null

    var val_ptr: *anyopaque = undefined;
    switch (union_value) {
        inline else => |x| val_ptr = @constCast(&x),
    }
    const T = @FieldType(UnionType, @tagName(desired_tag));
    return @as(*T, @ptrCast(@alignCast(val_ptr))).*;
}

any remarks ?

Dunno why the person above deleted his post because it was correct, you can do this like so: @field(union_value, @tagName(desired_tag))

2 Likes

Do you have a concrete example of the kind of code that this solves a problem for?

Generally, the trend for std.meta over the last few years has been to make it lighter, not to add new functionality to it (it’s possible that it might eventually be removed entirely). I don’t have a source on me right now but I also know there have been talks about bringing back @unionTag() because the userspace workaround is so verbose and awkward.

1 Like

I don’t understand what you mean here. Why are you avoiding the active_tag and for which parts specifically? (your first if actually uses it)

I don’t know what you are trying to do with all the conversion to *anyopaque and back to value. I think you could do this instead:

const std = @import("std");

fn retrieveFieldFromUnion(
    comptime UnionType: type,
    comptime desired_tag: std.meta.Tag(UnionType),
    union_value: UnionType, // may be a `*const` ?
) !@FieldType(
    UnionType,
    @tagName(desired_tag),
) {
    if (std.meta.activeTag(union_value) != desired_tag)
        return error.cantAccesInactiveTag; // TODO: may just return a null

    return switch (union_value) {
        inline else => |x| x,
    };
}

const Example = union(enum) {
    foo: u32,
    bar: u8,
};

pub fn main() !void {
    const f: Example = .{ .foo = 132323 };

    std.debug.print("value: {}\n", .{try retrieveFieldFromUnion(Example, .foo, f)});
}

But instead I would use one of those:

const std = @import("std");

const Example = union(enum) {
    foo: u32,
    bar: []const u8,

    pub fn unwrap(self: *const Example, comptime tag: std.meta.Tag(Example)) !std.meta.TagPayload(Example, tag) {
        if (std.meta.activeTag(self.*) != tag) return error.InvalidUnionAccess;
        return @field(self, @tagName(tag));
    }
};

pub fn GetUnion(T: type) type {
    return switch (@typeInfo(T)) {
        .pointer => |p| p.child,
        else => T,
    };
}
pub fn Tag(T: type) type {
    return std.meta.Tag(GetUnion(T));
}
pub fn activeTag(val: anytype) Tag(@TypeOf(val)) {
    return std.meta.activeTag(switch (@typeInfo(@TypeOf(val))) {
        .pointer => val.*,
        else => val,
    });
}
pub fn access(val: anytype, comptime tag: Tag(@TypeOf(val))) !std.meta.TagPayload(GetUnion(@TypeOf(val)), tag) {
    if (activeTag(val) != tag) return error.InvalidUnionAccess;
    return @field(val, @tagName(tag));
}

pub fn main() !void {
    const e: Example = .{ .bar = "hello" };

    std.debug.print("value: {s}\n", .{try e.unwrap(.bar)});
    std.debug.print("value: {s}\n", .{try access(&e, .bar)});
    std.debug.print("value: {s}\n", .{try access(e, .bar)});
}

However because tagged unions are already safety checked in safe modes, it isn’t really required to manually check for the active tag, so I would actually do this instead:

const std = @import("std");

const Example = union(enum) {
    foo: u32,
    bar: []const u8,

    pub fn unwrap(self: *const Example, comptime tag: std.meta.Tag(Example)) std.meta.TagPayload(Example, tag) {
        return @field(self, @tagName(tag));
    }
};

pub fn GetUnion(T: type) type {
    return switch (@typeInfo(T)) {
        .pointer => |p| p.child,
        else => T,
    };
}
pub fn Tag(T: type) type {
    return std.meta.Tag(GetUnion(T));
}
pub fn access(val: anytype, comptime tag: Tag(@TypeOf(val))) std.meta.TagPayload(GetUnion(@TypeOf(val)), tag) {
    return @field(val, @tagName(tag));
}

pub fn main() !void {
    const e: Example = .{ .bar = "hello" };
    const f: Example = .{ .foo = 45 };

    std.debug.print("value: {s}\n", .{e.unwrap(.bar)});
    std.debug.print("value: {s}\n", .{access(&e, .bar)});
    std.debug.print("value: {s}\n", .{access(e, .bar)});

    const lst = [_]Example{ e, f };
    for (lst) |v| {
        switch (v) {
            .bar => |val| std.debug.print("value: {s}\n", .{val}),
            inline else => |val| std.debug.print("value: {}\n", .{val}),
        }
    }
}

Then through testing and fuzzing you can make sure that your code handles all possible union values correctly.

2 Likes

Thanks. I just figured that out, and came to close the discussion. I didn’t noticed @field works on unions too. This is far more idiomatic.

fn retrieveFieldFromUnion2(
    comptime UnionType: type,
    comptime desired_tag: std.meta.Tag(UnionType),
    union_value: UnionType, // may be a `*const` ?
) !@FieldType(
    UnionType,
    @tagName(desired_tag),
) {
    if (std.meta.activeTag(union_value) != desired_tag)
        return error.cantAccesInactiveTag;
    return @field(union_value, @tagName(desired_tag));
}

How is using this function different than using switch?

1 Like

This can be further simplified:

const union_variant = @field(union_value, @tagName(desired_tag));

If you need to detect whether a string can be a valid tag for your union, I recommend StaticStringMap initialized with initComptime:

const m_union_variant = if (enum_map.get(str)) |tag| @field(union_value, tag) else null;
if (m_union_variant) |union_variant| {
    // ... etc
}
1 Like