Comparing tags of tagged unions

From this example

const TaggedUnion = union(enum) {
    one: void,
    two: void,
};

const p = @import("std").debug.print;

pub fn main() void {
    const x = TaggedUnion{.one = {}};
    const y = TaggedUnion{.two = {}};
    p("{}\n", .{x == .one}); // prints 'true'
    p("{}\n", .{x == .two}); // prints 'false'
    p("{}\n", .{y == .one}); // prints 'false'
    p("{}\n", .{y == .two}); // prints 'true'

    //p("{}\n", .{x == x}); // error: operator == not allowed for type 'TaggedUnion'
    //p("{}\n", .{x == y}); // error: operator == not allowed for type 'TaggedUnion'
    //p("{}\n", .{y == x}); // error: operator == not allowed for type 'TaggedUnion'
}

I learned that

  • comparing a whole instance of a tagged union with it’s kind is allowed
  • comparing two instances of a tagged union is not allowed

I do know that “tagged unions coerce to their tag type” (and this allows switching over tagged union instance as if we are switching over it’s kind), but this seems to me a bit contradictory.

Slices, for example, have kinda “automatic” len field.
Why don’t tagged unions have tag field?
Or maybe, there should be @tag builtin?

Is there some way to compare tags of two tagged union instances directly?..

You could use std.meta.activeTag(x), or more directly:

@as(
    @typeInfo(@TypeOf(x)).@"union".tag_type,
    x
)
5 Likes

thanks! just do not know much about std.meta.

1 Like

Then we can write

  • x == .one
  • activeTag(x) == .one

So these two are semantically the same.
And It gives (if we forget for a minute about coersion) an impression as if x and activeTag(x) are (kinda) same things… still I feel something is wrong with it.

What if there was no tagged uniion → it’s tag coercion?
We’d just write switch (activeTag(u)) and similar things. Why this coercion?

I’m not sure what you mean. activeTag is just performing coercion:

pub fn activeTag(u: anytype) Tag(@TypeOf(u)) {
    const T = @TypeOf(u);
    return @as(Tag(T), u); // Where Tag(T) = @typeinfo(T).@"union".tag_type
}

1 Like

I mean this coercion happens automatically when comparing with tags, so we can write both x == .one and activeTag(x) == .one, it’s the same, two ways of expressing the same. Without this coercion we’d always extract tags with activeTag()… or with x.tag (akin slice.len)… or with @tag(x). There’d be only one way.

I see. I suppose it is just the particular design decision that union tags are accessed via coercion.

While x == .one and activeTag(x) == .one are semantically the same, the coercion is just happening differently. In x == .one, x is complicitly coerced to it’s tag type due to the inferred type of .one, and with activeTag the coercion is made explicitly.

This is necessary in the x == y case as there’s no context that implies that either should be coerced to the tag type, although perhaps the compiler could be taught to infer that for this case.

1 Like

I understand it but do not understand why such decision was made.
To me personally it would be quite good to extract tags explicitly everywhere.
Not a big deal to type some extra letters :slight_smile: . And it would be more readable imho.

Ok.

Let it be here:

const TaggedUnion = union(enum) {
    one: void,
    two: void,
};

const p = @import("std").debug.print;
const activeTag = @import("std").meta.activeTag;

pub fn main() void {
    const x = TaggedUnion{.one = {}};
    const y = TaggedUnion{.two = {}};

    // you don't have to use 'activeTag' in switch
    switch (x) {
        .one => |_| p("x is ONE\n", .{}), // *
        .two => |_| p("x is TWO\n", .{}),
    }

    // but you may if you wish
    switch (activeTag(y)) {
        .one => |_| p("y is ONE\n", .{}),
        .two => |_| p("y is TWO\n", .{}), // *
    }

    // same when comparing unions with tags
    // you may use activeTag or you may not
    p("{}\n", .{x == .one}); // prints 'true'
    p("{}\n", .{activeTag(y) == .two}); // prints 'true'

    // but when comparing two unions
    // you HAVE to extract tags manually
    p("{}\n", .{activeTag(x) == activeTag(y)}); // ok
//    p("{}\n", .{x == y}); // error: operator == not allowed for type 'TaggedUnion'
}

You could put that in a convenience method of the tagged union:

const TaggedUnion = union(enum) {
    one,
    two,

    fn eql(self: TaggedUnion, other: TaggedUnion) bool {
        return activeTag(self) == activeTag(other);
    }
};

// Now you can do x.eql(y)
1 Like

Initially I placed this topic into “Explain” category.
But after @n0s4 told about activeTag() I changed my mind,
moved it to “Help” and marked @n0s4’s reply as solution
(and let it be so, it’s really is, when one needs to compare tags by some reason).

But still automatic tagged union to tag coercion puzzles me… :frowning:
For me explicit tu.tag (or similar) everywhere would be better.
And now I have an itch to open new topic (“Why tagged union to tag coercion?” or so),
in “Explain”, but I’m not sure whether it’s worth it or not.

I am not alone
:expressionless: