When handling errors in polymorphic code, sometimes we run into situations where particular errors are absent from an inferred error set. Consider the following:
const std = @import("std");
fn foo() !void {
return error.FooHappened;
}
fn bar() !void {
return error.BarHappened;
}
fn call(cb: anytype) void {
cb() catch |err| {
switch (err) {
error.FooHappened => std.debug.print("foo happened\n", .{}),
error.BarHappened => std.debug.print("bar happened\n", .{}),
}
};
}
pub fn main() !void {
call(foo);
call(bar);
}
This wouldn’t compile since the set returned by foo() doesn’t contain error.BarHappened and bar()'s set is missing error.FooHappened. While we can work around this by making foo() and bar() return the combined set, that’s undesirable since the resulting binary would end up with unreachable prongs.
Similar situations can arise when working with enums. The ability to disable particular switch cases based on comptime conditions is generally useful. In C, this is pretty easy:
case TURTLE: return milkTurtle();
#ifdef UNICORN
case UNICORN: return milkUnicorn();
#end
case ZEBRA: return milkZebra();
One way we can implement the feature is to use undefined to denote the absence of a case:
fn call(cb: anytype) void {
cb() catch |err| {
const E = @TypeOf(err);
switch (err) {
ifPartOf(error.FooHappened, E) => std.debug.print("foo happened\n", .{}),
ifPartOf(error.BarHappened, E) => std.debug.print("bar happened\n", .{}),
}
};
}
fn ifPartOf(comptime err: anytype, comptime Set: anytype) Set {
const errors = @typeInfo(Set).error_set orelse return err;
return inline for (errors) |e| {
if (std.mem.eql(u8, @errorName(err), e.name)) break err;
} else undefined;
}
I think this is fairly intuitive. undefined is not a legal value, ergo the case should be tossed out. The left side being left accidentally undefined is unlikely.