Try @enumFromInt() and get error when corresponding value does not exist

Hello.
I love Zig’s enum type that gives us the wonderful exhaustive check of switch. However, I sometimes want to try to convert an integer that has no value in the enum type:

const Fruit = enum (u32) {
  apple = 0x1,
  orange = 0x2,
};
fn handleFruit (index: u32) void {
  switch (@enumFromInt(index)) {...}
}

When I pass 3 to handleFruit(), it leads to a panic in Debug build and UB in Release build. I want some method that converts an integer to the enum, but returns an error or something when it cannot, like:

const result: ?Fruit = if (tryEnumFromInt(index)) |fruit| fruit else null;

In my case, my enum type has enormous values. My code takes an integer from user inputs, converts them into the enum type, and then handle them in a switch. But I don’t have to implement all the values and can just ignore when it is allowed, though I plan to implement some of the unhandled enum values in the near future.

Note that my enum has “gaps” between its values:

enum (u32) {
  first = 0,
  second = 3,
  third = 9,
};

So I cannot check the input by comparing the minimum and maximum values of the enum using @typeInfo().

I really appreciate your advice, thank you.

1 Like

Use std.meta.intToEnum.

1 Like

That was my first call as well. It won’t work in this case, though, since it still ends up using @enumFromInt internally.

That’s the very what I wanted! Thank you!

My code becomes:

if (std.meta.intToEnum(index)) |fruit| switch (fruit) {...} else |_| {...}

It looks great :slight_smile:

If I pass a comptime value not in the enum to std.mem.intToEnum(), it becomes compile error (it is desirable). But if index is a runtime-value, it worked perfectly for me. (Am I misunderstanding something…?)

1 Like

I assumed that you wanted smth like this to work (but it doesn’t):

const std = @import("std");

const Fruit = enum(u32) {
    apple = 1,
    orange = 2,
};

test Fruit {
    try std.testing.expectError(error.InvalidEnumTag, std.meta.intToEnum(Fruit, 3));
}

But yeah this does (my bad):

const std = @import("std");

const Fruit = enum(u32) {
    apple = 1,
    orange = 2,
};

test Fruit {
    const index: u8 = 3;
    try std.testing.expectError(error.InvalidEnumTag, std.meta.intToEnum(Fruit, index));
}
1 Like

Given the description of the problem domain, I suspect what you actually want is a non-exhaustive enum.

const Fruit = enum (u32) {
    apple = 0x1,
    orange = 0x2,
    _, // <- this means non-exhaustive 
};
fn handleFruit (index: u32) void {
    switch (@enumFromInt(index)) {
         .apple => doAppleStuff(),
         .orange => doOrangeStuff(),
         _  =>  |unknown_fruit| => handleUnknown(unknown_fruit),
   }
}

Using _ instead of else in the switch statement means that your switch has to handle all known values of the enum, so when you add more later, you have to add them to all exhaustive switches or it won’t compile.

Using std.meta.intToEnum and treating unknown values as an error might work, but a non-exhaustive enum with a _ prong is idiomatic here, when you say this:

An unknown value isn’t really an error, is it? The big difference is that if an unknown value is given to std.meta.intToEnum it will throw/return an error{IntToEnumError}, but using _ will give you a properly-typed enum value for the unknowns, you can log them, convert them to the backing integer and switch on that, there are a lot of options which you don’t get from treating them as an error condition.

Mostly I wanted to convey that you can still do exhaustive switching with a non-exhaustive enum, because a _ prong still requires you to handle all named values explicitly.

3 Likes

Wow, I didn’t know non-exhaustive enum! That seems a little bit more preferable in my situation.
I have to read the whole Zig documentation again. It has much valuable features that I don’t know…
I’m sorry but I’ve changed the “solution” to mnemnion’s answer.

1 Like