Nested tagged unions

This is a continuation of this question. Suppose I want to describe results (values) of some measurements. We have physical quantities, each quantity (length, mass, force etc) can be measured in various units (grams, kilograms etc for mass and whatnot). Of course, this can be modeled in a number of ways, I constructed this (as an exercise with… say, “multi-level” tagged unions):

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

const Value = struct {
    const Quantity = union(enum) {
        const DistUnit = union(enum) {
            mm: void,
             m: void,
            km: void,
        };
        const TimeUnit = union(enum) {
            s: void,
          min: void,
            h: void
        };
        const MassUnit = union(enum) {
            mg: void,
             g: void,
            kg: void,
        };
        dist_in: DistUnit,
        time_in: TimeUnit,
        mass_in: MassUnit,
    };

    name: []const u8,
    q: Quantity,
    v: f32,

    fn quantityName(v: Value) []const u8 {
        return @tagName(v.q);
    }

    fn unitName(v: Value) []const u8 {
        return switch (v.q) {
            inline else => |u| @tagName(u),
        };
    }

    fn isEquatableTo(this: Value, other: Value) bool {

        if (tag(this.q) != tag(other.q))
            return false;

//        return switch (this.q) {   // failure
//            inline else => |tu| tu,
//        } == switch (other.q) {
//            inline else => |ou| ou,
//        };

//        const aa = switch (this.q) { // failure
//            inline else => |u| u,
//        };
//        const bb = switch (other.q) {
//            inline else => |u| u,
//        };
//        return aa == bb;

        return true;
    }
};

In Value.isEquatableTo() function I want to check if two values are “the same” meaning

  • they are of same quantity (mass is not distance for ex)
  • they measured in same units (kg is not mg for ex)

First part is ok, I stumbled at the second. I messed with inline else, but failed to compare unit tags. How to do this?!?

Also there is a thing which I was not able to understand.
Here is main function:

pub fn main() !void {
    const v = Value{.name = "tower-height", .q = .{.dist_in = .m}, .v = 27.5};
    const a = Value{.name = "apple-mass", .q = .{.mass_in = .kg}, .v = 0.2};
    p("{s} is {} {s}\n", .{v.name, v.v, v.unitName()});
    p("{s} is {} {s}\n", .{a.name, a.v, a.unitName()});
    p("{} // {s} {s}\n", .{v.isEquatableTo(a), v.quantityName(), a.quantityName()});
//    p("{} // {s} {s}\n", .{v.isEquatableTo(v), v.quantityName(), v.quantityName()});

    const x = switch (v.q) {
        inline else => |u| u,
    };
    p("{}\n", .{x});
}

The last part with switch works fine here, but when similar statement is used in isEquatableTo, compiler says:

4.zig:57:20: error: incompatible types: '4.Value.Quantity.DistUnit' and '4.Value.Quantity.TimeUnit'
        const aa = switch (this.q) { // failure
                   ^~~~~~
4.zig:58:32: note: type '4.Value.Quantity.DistUnit' here
            inline else => |u| u,
                               ^
4.zig:58:32: note: type '4.Value.Quantity.TimeUnit' here
            inline else => |u| u,
                               ^

Why is it so?

It works fine here because v.q is comptime-known, so it only evaluates one branch of the switch statement. Whereas if it was runtime known (try changing v to var), like inside a function call, then you cannot assign the result to a variable because there are three different result types.

As for the actual problem: The key is to evaluate both unions inside the same switch statement. You are allowed to do this since you checked beforehand that they have the same tag. I think something like this would work:

...
switch (tag(v.q)) {
    inline else => |t| return @field(this, @tagName(t)) == @field(other, @tagName(t)),
}

Another note:

const TimeUnit = union(enum) {
    s: void,
    min: void,
    h: void
};

If all your union types are void, why not just use an enum? This should make it a bit easier to deal with.

const TimeUnit = enum {s, min, h};
2 Likes

I have such “model”, just for the moment for some irrational reason I wanted to experiment with this model. :slight_smile:

Probably you meant

    fn isEquatableTo(this: Value, other: Value) bool {

        if (tag(this.q) != tag(other.q))
            return false;

        switch (this.q) { // not tag(this.q)
            inline else => |u|
                return @field(this, @tagName(u)) == @field(other, @tagName(u)),
        }
    }

(tag() is just an alias for std.meta.activeTag(), it won’t do here)

but anyway, same story:

4.zig:53:46: error: unable to evaluate comptime expression
                return @field(this, @tagName(u)) == @field(other, @tagName(u)),
                                             ^

I think you want something more like this:

switch (this.q) { // not tag(this.q)
    inline else => |u, tag|
        return u == @field(other.q, @tagName(tag)),
}

related topic:

1 Like

Yes, I saw similar patterns here and in some other places around the forum, but have not thought out how to apply them in my case.

ok,

    fn isEquatableTo(this: Value, other: Value) bool {

        if (tag(this.q) != tag(other.q))
            return false;

        switch (this.q) {
            inline else => |u, t|
                return u == @field(other.q, @tagName(t)),
        }
    }

Now compiler says

4.zig:59:26: error: operator == not allowed for type '4.Value.Quantity.DistUnit'
                return u == @field(other.q, @tagName(t)),
                       ~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Easiest would be to simplify the union to enums like @IntegratedQuantum suggested, otherwise this works:

fn isEquatableTo(this: Value, other: Value) bool {
    if (tag(this.q) != tag(other.q))
        return false;

    switch (this.q) {
        inline else => |u, t| return tag(u) == tag(@field(other.q, @tagName(t))),
    }
}

It gets confusing with the nested unions and multiple tags.

2 Likes

Yes, I’ll try my other model later.

Ohhh, thanx a lot!!!
Will mark as solution.

1 Like

In the very first version I had just two simple enums, one for physical quantities, one for units of measurement. Value had two fields, q and u, so it was very easy to check for equatability, just v1.q == v2.q and v1.u == v2.u. But with this plain construction one can easily measure length in kilograms etc; there must be some quantity-unit “affinity”/“compatibility” table or so.

And with these multi-level tagged unions you can’t measure distance in units of mass, it just won’t compile:

const th = Value{.name = "tree-height", .q = .{.dist_in = .kg}, .v = 2.5};
4.zig:72:60: error: no field named 'kg' in enum '@typeInfo(4.Value.Quantity.DistUnit).Union.tag_type.?'
    const b = Value{.name = "tree-height", .q = .{.dist_in = .kg}, .v = 2.5};
                                                  ~~~~~~~~~^~~~~

So that was the reason of using nested (multi-level) constructions.

1 Like

What I was suggesting is to replace the inner-most unions with enums. You’d still use a nested construct with all of its benefits, just your inner level would be an enum instead of a union.

1 Like

I see, I just described the “evolution” of my thoughts.

Nesting is needed just because Q:U is one-to-many relation.

btw, this

        return switch (this.q) {
            inline else => |u, t|
                tag(u) == tag(@field(other.q, @tagName(t))),
        };

seems to work just as well.
I am a bit in a maze… which one is “better”? :slight_smile:

Yes, here it is:

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

const Value = struct {

    const Quantity = enum {
        dist, // short for 'distance'
        time,
        mass,
    };

    const DistUnit = enum {
        mm, m, km,
    };

    const TimeUnit = enum {
        s, min, h,
    };

    const MassUnit = enum {
        mg, g, kg,
    };

    const UnitOf = union(Quantity) {
        dist: DistUnit,
        time: TimeUnit,
        mass: MassUnit,
    };

    nam: []const u8,
    uof: UnitOf,
    val: f32,

    fn quantityName(v: Value) []const u8 {
        return @tagName(v.uof);
    }

    fn unitName(v: Value) []const u8 {
        return switch (v.uof) {
            inline else => |u| @tagName(u),
        };
    }

    fn isEquatableTo(self: Value, other: Value) bool {
        if (tag(self.uof) != tag(other.uof))
            return false;

        return switch (self.uof) {
            inline else => |u, t|
                u == @field(other.uof, @tagName(t)),
        };
    }
};

pub fn main() !void {
    const a = Value {.nam = "melon-1-mass", .uof = .{.mass = .kg}, .val = 3.5};
    const b = Value {.nam = "melon-2-mass", .uof = .{.mass = .g}, .val = 2900.0};
    p("{s} is {} {s}\n", .{a.nam, a.val, a.unitName()});
    p("{s} is {} {s}\n", .{b.nam, b.val, b.unitName()});
    const yn = if (a.isEquatableTo(b)) "yes" else "no";
    p("is {s} comparable to {s}? {s}\n", .{a.nam, b.nam, yn});
}
2 Likes

I understand you prefer to be explicit and clear in your source code, but note that you can consolidate all those enums into just one tagged union literal type definition:

    uof: union(enum) {
        dist: enum { mm, m, km },
        time: enum { s, min, h },
        mass: enum { mg, g, kg },
    },
4 Likes

I would keep enum for quantities:

    const Quantity = enum {
        dist, // short for 'distance'
        time, // you can place your philosophy of what "time" is here :-)
        mass, 
    };

    const UnitOf = union(Quantity) { // better than just union(enum)
        dist: enum {mm, m, km},
        time: enum {s, min, h},
        mass: enum {mg, g, kg},
    };

semicolon makes the difference…

        switch (self.uof) {
            inline else => |u, t|
                return u == @field(other.uof, @tagName(t)),
        } // no ';' ok
        return switch (self.uof) {
            inline else => |u, t|
                u == @field(other.uof, @tagName(t)),
        }; // without ';' - // error: expected ';' after statement

is it intended?

I believe the rule of thumb here is that if it produces a value, it requires a semicolon. If it doesn’t produce a value and thus is control-flow only, it doesn’t allow a semicolon. This also happens with if, for, and while which can be control-flow-only (producing no value) and not allowing semicolon, versus their value-producing forms where the semicolon is required.

2 Likes

there was a topic
:expressionless:

1 Like