Functions inside enums

I was wondering if there is much use in enum functions/methods.
Consider this simple example.

const std = @import("std");
const expect = std.testing.expect;

const Kind = enum {
    k1,
    k2,
    k3,

    fn is(kind: Kind, variant: Kind) bool {
        return kind == variant;
    }

    fn isNot(kind: Kind, variant: Kind) bool {
        return kind != variant;
    }
};

test "test kinds" {
    const k: Kind = .k1;
    try expect(k.is(.k1) == true);
    try expect(k.is(.k2) == false);
    try expect(k.is(.k3) == false);

    try expect(k.isNot(.k1) == false);
    try expect(k.isNot(.k2) == true);
    try expect(k.isNot(.k3) == true);
}

Well, it looks kinda nice, butā€¦
Can anybody imagine something besides those is() and isNot() (which are in fact just wrappers around comparison operators)?

I guess this is a situation where not having the feature would be a ā€˜fault of imaginationā€™ given the probability that someone in the future will need it for something important. Pretty printing isnā€™t that critical but comes in handy once in a while:

const std = @import("std");

pub const Token = enum {
    cr,
    lf,

    // Pretty printing
    pub fn format(self: Token, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
        switch (self) {
            .cr => try writer.writeAll("carriage return"),
            .lf => try writer.writeAll("line feed"),
        }
    }
};

pub fn main() void {
    const t1 = Token.cr;
    const t2 = Token.lf;
    std.log.info("{}, {}", .{ t1, t2 });
}
1 Like

I have implemented an enum with compass directions N, S, E, W, and ā€œmethodsā€ such as turnLeft() (N => W), reverse() (N => S), etc. The enums were pretty handy for this.

2 Likes

Nice example!

1 Like

Ok, so there are two kinds of enum variables:

  • those that can not change their value (when used for tagging unions for ex.)
  • those that can, as in your example.

I had my mind set on the first case by some reasonā€¦
But it seems to me the ability to turn is rather a property of moving thing but not of a direction, I mean

const Direction = enum {
    N,
    E,
    S,
    W,
};

const MovingThing = struct {
    dir: Direction,
    fn turnLeft(self: *MovingThing) void {}
};

It is a moving thing that can turn (not a direction itself), so turnLeft() as a method of that moving thing is a better ā€œontologyā€ as I see it.

My methods would simply return the correct enum value, so no mutability.

I am not saying this is correct, proper or good style; it just seemed to work fine for my purposes.

But then you are assigning the value returned to some var/struct field, arenā€™t you?

Neither do I about my ā€œinterpetationā€. :slight_smile: Embedding turnLeft() and alike into Direction enum has an advantage that you can then use these methods in any struct with a field of type Direction. In my ā€œapproachā€ I would have to duplicate them in every struct.

Right, I would do stuff like this:

const forward = player.direction();
const backward = forward.reverse();
// do something with forward and backward

This sort of thing. Cheers!

This depends on the richness and complexity of the space you are modeling.

The more your enums look like ā€œnames for integers,ā€ the less use you will have for bound methods. But consider playing cards: they are frequently used as an example for enums, and there are plenty of questions you might ask about a playing card. Is this a face card, what is the rank, what is the suite, what is the score of this card (depends on the game, obviously)? What nicknames does this card have? What aliases? Does the card have a name or just a descriptor? (Graceā€™s card, the curse of Scotland, a ā€œsuicide kingā€, ā€œthe man with the axeā€, ā€œlady Luckā€)

Or consider file/directory permissions. On ā€œsimpleā€ unix systems, there is still the umask value to be factored in, so an ā€œeffectiveā€ value might be quite different from the actual value. In an ACL environment, with cascading settings from higher levels, this may be quite challenging. Is this still the domain of an enum? Thatā€™s an implementation thing. But thereā€™s likely an enum in there somewhere.

Or, what about HTTP request methods? (GET, POST, DELETE, etc.) There is a big collection of attributes around those values - can they be cached, shared, etc. Thereā€™s enough complexity that the answers probably arenā€™t found in a table. Itā€™s worth writing a function to determine the answers (some of which depend on the individual request). It might not be an enum-level function, but thatā€™s implementorā€™s choice.

Finally, what about enumerated values? Objects that arenā€™t integers at all, but that we can still meaningfully talk about in shorthand. Toolchain? Gcc vs. clang. Binaries? Elf vs. Dwarf. Boxers or briefs? Paper or plastic? Fight or flight? Craps or Roulette? BMW or Aston-Martin? There are plenty of ā€œthingsā€ we model with potentially infinitely-many values, but practically only a few. Again, that may be an enum or a tagged enum with an ā€œOtherā€ option, or something else. But it certainly could be an enum in version 1 of the project, and have lots of operations and queries to interrogate.

@aghast thank you for your comprehensive answer! But let me ask - are there (non-trivial) examples of enum ā€œmethodsā€ in Zig stdlib for the moment? (itā€™s not easy to find it out by grep)