Tree of types using enums?

I’ll try to give an example of what I’m trying to achieve:

const Pet = enum {
    Cat, // 0
    CatBreed1,
    CatBreed2,
    CatBreed2Sub1,
    CatBreed2Sub2,
    Dog,
    DogBreed1,
    DogBreed2, // 7
};

// A namespaced enum?

const Pet = enum {
    Cat = enum { // 0
        Breed1, // 1
        Breed2 = enum { // 2
            Sub1, // 3
            Sub2, // 4
        },
    },
    Dog, // 5
    DogBreed1, // 6
    DogBreed2, // 7
};

// Pet.Cat = 0
// Pet.Cat.Breed2 = 2
// Pet.Cat.Breed2.Sub2 = 4

// Why? Suppose that nested enums could work as follows:

const cat = Pet; // cat.is(Pet) -- true
const cat = Pet.Cat; // cat.is(Pet), cat.is(Pet.Cat) -- all true
const cat = Pet.Cat.Breed1; // cat.is(Pet), cat.is(Pet.Cat), cat.is(Pet.Cat.Breed1) -- all true

Basically, I need a tree of types, where types can be “specialized” and be compared against their generic ascendants. Not sure how to think about it (organize) in terms of Zig.

1 Like

Union enums would be the way:

const std = @import("std");

pub const Pet = union(enum) {
    pub const Cat = union(enum) {
        siberian: void,
        birman: void,
    };

    pub const Dog = union(enum) {
        pub const Shepherd = union(enum) {
            australian: void,
            german: void,
        };

        shepherd: Shepherd,
        dalmatian: void,
    };

    cat: Cat,
    dog: Dog,
};

test Pet {
    const pet = Pet{ .cat = .siberian };

    try std.testing.expectEqual(std.meta.activeTag(pet), .cat);
    try std.testing.expectEqual(pet.cat, .siberian);
}
1 Like

I though about it but things get tricky… For example, trying to init shepherd dog:

test Pet {
    const pet = Pet{ .dog = .shepherd };
    _ = pet;
}

Gives:

howtos/q_enumTree.zig:59:27: error: coercion from enum '@TypeOf(.enum_literal)' to union 'q_enumTree.Pet.Dog' must initialize 'q_enumTree.Pet.Dog.Shepherd' field 'shepherd'
    const pet = Pet{ .dog = .shepherd };
                     ~~~~~^~~~~~~~~~~
howtos/q_enumTree.zig:50:9: note: field 'shepherd' declared here
        shepherd: Shepherd,
        ^~~~~~~~~~~~~~~~~~
howtos/q_enumTree.zig:44:17: note: union declared here
    const Dog = union(enum) {
                ^~~~~

Even if there wasn’t an error, I wouldn’t know how to initalize, say, the .australian dog.

1 Like

Oh, I think you’re just confused about the syntax: union enum fields of type void can be accessed and assigned just like enum tags, which is more readable.

const std = @import("std");

const Pet = union(enum) {
    const Cat = union(enum) {
        siberian: void,
        birman: void,
    };

    const Dog = union(enum) {
        const Shepherd = union(enum) {
            australian: void,
            german: void,
        };

        shepherd: Shepherd,
        dalmatian: void,
    };

    cat: Cat,
    dog: Dog,
};

test Pet {
    // Short syntax
    const pet = Pet{ .dog = .{ .shepherd = .australian } };
    // Full syntax
    // const pet = Pet{ .dog = .{ .shepherd = .{ .australian = {} } } };

    try std.testing.expectEqual(std.meta.activeTag(pet), .dog);
    try std.testing.expectEqual(std.meta.activeTag(pet.dog), .shepherd);
    try std.testing.expectEqual(pet.dog.shepherd, .australian);
}
3 Likes

That’s interesting! I wouldn’t guess it myself. However, I don’t understand how this kind of comparison works:

try std.testing.expectEqual(std.meta.activeTag(pet.dog), .shepherd);

For reference, I put the source code of the parts of potential interest:

// std/testing.zig

pub fn expectEqual(expected: anytype, actual: @TypeOf(expected)) !void {
    switch (@typeInfo(@TypeOf(actual))) {
        .Bool,
        .Int,
        .Float,
        .ComptimeFloat,
        .ComptimeInt,
        .EnumLiteral,
        .Enum,
        .Fn,
        .ErrorSet,
        => {
            if (actual != expected) {
                print("expected {}, found {}\n", .{ expected, actual });
                return error.TestExpectedEqual;
            }
        },
        // ...
// std/meta.zig

/// Returns the active tag of a tagged union
pub fn activeTag(u: anytype) Tag(@TypeOf(u)) {
    const T = @TypeOf(u);
    return @as(Tag(T), u);
}

pub fn Tag(comptime T: type) type {
    return switch (@typeInfo(T)) {
        .Enum => |info| info.tag_type,
        .Union => |info| info.tag_type orelse @compileError(@typeName(T) ++ " has no tag type"),
        else => @compileError("expected enum or union type, found '" ++ @typeName(T) ++ "'"),
    };
}

I read the above several times but I’m still straggling to get the basics. For me, Tag() returns the underlaying type of a enum (the type that represents enumeration number) that is attached to the union (say, u2 for the union of the length 4). But we, somehow, compare that type with a specific enum “field”, say, .shepherd. How is that?

The keywords for the type declaration is very helpful here: union(enum), meaning we declare both a union and an enum in one go, which explains why names of union fields match the values of enum tags.

In general, union(enum) is used whenever you want to switch on union fields, which means you’ll need tags for fields, so an enum with values matching the names of union fields. The simplest way to achieve that is change union declaration to union(enum).

I get the logic you explained. Thank you. I think my main issue was to understand the magic behind activeTag(). It is just twisted. I ended up thinking about it the following way (please, correct me if I’m wrong):

When we refer to pet variable, we retrieve the active field .dog, which is a number from the enum tagged to union of the type @typeInfo(@TypeOf(pet)).Union.tag_type. Performing activeTag() on that number (or the underlaying .dog), we kind of casting it to the Enum space so that when we compare it with the literal .dog detached from any context, we won’t get type mismatch.

Yeah, so we don’t get the type mismatch here:

try std.testing.expectEqual(std.meta.activeTag(pet.dog), .shepherd);

, because the expectEqual’s second argument’s type is determined based on the first argument’s type. So expectEqual first looks at the type of activeTag(pet.dog) which is an enum with two tag values: .shepherd and .dalmation, and then expectEqual checks that the second argument is of the same type, which it is since it’s .shepherd.

As a sidenote, if we were to swap the order of arguments, we’d get a compile error since there’s no way of inferring the enum type of a tag value .shepherd. To fix it we’d have to specify the first argument’s type, which would make the statement more verbose becoming:

try std.testing.expectEqual(Pet.Dog.shepherd, std.meta.activeTag(pet.dog));
2 Likes

Here’s a code snippet that may help clear things up a bit, too. I’m just adding print statements:

const std = @import("std");

const print = std.debug.print;

/// Returns the active tag of a tagged union
pub fn activeTag(u: anytype) Tag(@TypeOf(u)) {
    const T = @TypeOf(u);

    print("\nType of activeTag argument: {s}\n", .{ @typeName(T) });

    const U = Tag(T);

    print("\nType of Tag(T): {s}\n", .{ @typeName(U) });
    
    return @as(U, u);
}

pub fn Tag(comptime T: type) type {
    return switch (@typeInfo(T)) {
        .Enum => |info| info.tag_type,
        .Union => |info| info.tag_type orelse @compileError(@typeName(T) ++ " has no tag type"),
        else => @compileError("expected enum or union type, found '" ++ @typeName(T) ++ "'"),
    };
}

const TestEnum = enum {
    A,
    B,
    C
};

const TestUnion = union(TestEnum) {
    A: void, 
    B: void, 
    C: void
};

pub fn main() !void {

    const x = TestUnion{ .A = {} };

    const result: TestEnum = activeTag(x);
    
    print("\nType of Result: {s}\n", .{ @typeName(@TypeOf(result)) });

    print("\nResult Value: {}\n", .{ result });
}
1 Like

Here’s another approach. Doesn’t give you the syntax that you want but it does yield the right relations.

const std = @import("std");

const Pet = enum {
    Cat,
    @"Cat > Breed1",
    @"Cat > Breed2",
    @"Cat > Breed2 > Sub1",
    @"Cat > Breed2 > Sub2",
    Dog,
    @"Dog > Breed1",
    @"Dog > Breed2",

    pub const parents = createParents();

    fn getDepth(comptime e: @This()) comptime_int {
        const name = @tagName(e);
        comptime var depth = 0;
        inline for (name) |c| {
            if (c == '>') {
                depth += 1;
            }
        }
        return depth;
    }

    fn createParents() []?@This() {
        const fields = @typeInfo(@This()).Enum.fields;
        var enums: [fields.len]?@This() = undefined;
        inline for (fields) |field| {
            const e = @field(@This(), field.name);
            enums[field.value] = findParent(e);
        }
        return &enums;
    }

    fn findParent(comptime e: @This()) ?@This() {
        var candidate: ?@This() = null;
        const fields = @typeInfo(@This()).Enum.fields;
        const depth = getDepth(e);
        inline for (fields) |field| {
            const other = @field(@This(), field.name);
            if (other == e) {
                return candidate;
            }
            const other_depth = getDepth(other);
            if (other_depth == depth - 1) {
                candidate = other;
            }
        }
        return null;
    }

    pub fn is(self: @This(), other: @This()) bool {
        if (self == other) {
            return true;
        }
        const index = @intFromEnum(self);
        if (parents[index]) |parent| {
            if (parent == other) {
                return true;
            }
            return parent.is(other);
        }
        return false;
    }
};

test "Pet" {
    std.debug.assert(Pet.Cat.is(Pet.Cat) == true);
    std.debug.assert(Pet.@"Cat > Breed1".is(Pet.Cat) == true);
    std.debug.assert(Pet.@"Cat > Breed2 > Sub1".is(Pet.@"Cat > Breed2") == true);
    std.debug.assert(Pet.@"Cat > Breed2 > Sub1".is(Pet.@"Cat > Breed1") == false);
    std.debug.assert(Pet.@"Cat > Breed2 > Sub1".is(Pet.Dog) == false);
}

Relying on the items being in the right order.

1 Like