Union Equality

I’m trying to represent a dynamically typed value in Zig, and need to be able to check if two of such values are equal, both in type and in value. I have a function that seems to work but want to know if there is any more efficient way to do this, rather than having a potentially long switch statement for every variant. Thanks.

pub const Value = union(enum) {
    pub const Type = std.meta.Tag(Value);

    nil,
    boolean: bool,
    number: f32,
    // ...

    pub fn equals(self: Value, other: Value) bool {
        if (@as(Type, self) != @as(Type, other)) return false;
        return switch (self) {
            .nil => true,
            .number => self.number == other.number,
            .boolean => self.boolean == other.boolean,
            // ...
        };
    }
}

You can use std.meta.eql, or look how that is implemented:

        .Union => |info| {
            if (info.tag_type) |UnionTag| {
                const tag_a = activeTag(a);
                const tag_b = activeTag(b);
                if (tag_a != tag_b) return false;

                inline for (info.fields) |field_info| {
                    if (@field(UnionTag, field_info.name) == tag_a) {
                        return eql(@field(a, field_info.name), @field(b, field_info.name));
                    }
                }
                return false;
            }

            @compileError("cannot compare untagged union type " ++ @typeName(T));
        },

6 Likes

Hm, isn’t that a bit of dead code? that return false should be unreachable, no?

Aside from using a library function, yes, you probably want multiple prongs of a switch statement. At least for numeric comparisons, since comparing floats is not a straightforward check on whether the bits are equal, there is also the fact that -0 and +0 are equal, NaN’s always being unequal to everything, even themselves, and maybe there’s more. So when you do a == b on float types it is going to give you a completely different instruction that couldn’t be used for any other type.

You could try an inline else though to have Zig fill in the details.

3 Likes

Thank you, I think I’ll use this.

I think this is simpler?

    pub fn equals(self: Value, other: Value) bool {
        return switch (other) {
            .nil => self == .nil,
            .number => |n| self == .number and self.number == n,
            .boolean => |b| self == .boolean and self.boolean == b,
            // ...
        };
    }

If you don’t mind panicking when the tags are different, you can remove the first part of the tests:

.number => |n| self.number == n, // will panic if self isn't .number

I think this is kind of neat:

    pub fn equals(self: Value, other: Value) bool {
        return switch (self) {
            inline else => |value, tag| tag == std.meta.activeTag(other) and value == @field(other, @tagName(tag)),
        };
    }

This works because for nil it just compares that {} is equal to {} which is true.


If any case should compare values differently like for example float, you can add it as an explicit case like this:

    pub fn equals(self: Value, other: Value) bool {
        return switch (self) {
            .float => |f| .float == std.meta.activeTag(other) and floatEqualImpl(f, other.float),
            inline else => |value, tag| tag == std.meta.activeTag(other) and value == @field(other, @tagName(tag)),
        };
    }
6 Likes

@Sze wins. Flawless victory. :smile_cat:

1 Like