That’s something I made today and I thought that some other people might want to use something like it. It works on enums, it doesn’t work on tagged unions when you want to capture.
Example of using it:
const EnumA = enum (u1) {
Red,
Black,
};
const EnumB = enum(u1) {
Up,
Down,
};
pub fn main() !void {
const a: EnumA = .Red;
const b: EnumB = .Down;
const s = switch (match(.{a, b})) {
.case(.{.Red, .Up}) => "red up",
.case(.{.Red, .Down}) => "red down",
.case(.{.Black, .Up}) => "black up",
.case(.{.Black, .Down}) => "black down",
};
std.debug.print("{s}\n", .{s});
}
Implementation is like this:
fn MatcherStruct(types: []const type) type {
for (types) |t| {
if (@typeInfo(t) != .@"enum") @compileError("Types must be enums");
}
const N = types.len;
const fieldNames: [N] []const u8 = comptime D: {
var ans: [N][]const u8 = undefined;
for (0..N) |i| {
ans[i] = std.fmt.comptimePrint("{}", .{i});
}
break :D ans;
};
const Generated = @Struct(.@"packed", null, &fieldNames, types, &@splat(.{}));
return packed struct {
inner: Generated,
inline fn case(args: @Tuple(types)) @This() {
var ans: Generated = @bitCast(@as(@typeInfo(Generated).@"struct".backing_integer.?, 0));
inline for (0..N) |i| {
@field(ans, std.fmt.comptimePrint("{}", .{i})) = args[i];
}
return .{ .inner = ans };
}
};
}
fn match(args: anytype) MatcherStruct(D: {
var ans: []const type = &.{};
for (args) |arg| {
ans = ans ++ &[_]type{@TypeOf(arg)};
}
break :D ans;
}) {
return .case(args);
}
If you don’t want to type the .{} inside case, AFAIK there’s no way to solve it for the general case of “N” enum values because zig doesn’t support variadic function arguments. However you can hand roll implementations for any reasonable number of values:
fn MatcherStruct2(types: [2]type) type {
return packed struct {
@"0": types[0],
@"1": types[1],
inline fn case(@"0": types[0], @"1": types[1]) @This() {
var ans: @This() = @bitCast(@as(@typeInfo(@This()).@"struct".backing_integer.?, 0));
ans.@"0" = @"0";
ans.@"1" = @"1";
return ans;
}
};
}
fn MatcherStruct3(types: [3]type) type {
return packed struct {
@"0": types[0],
@"1": types[1],
@"2": types[2],
inline fn case(@"0": types[0], @"1": types[1], @"2": types[2]) @This() {
var ans: @This() = @bitCast(@as(@typeInfo(@This()).@"struct".backing_integer.?, 0));
ans.@"0" = @"0";
ans.@"1" = @"1";
ans.@"2" = @"2";
return ans;
}
};
}
fn MatcherStructN(args: anytype) type {
var types: []const type = &.{};
inline for (args) |arg| {
types = types ++ &[_]type{@TypeOf(arg)};
}
return switch (types.len) {
2 => MatcherStruct2(types[0..2].*),
3 => MatcherStruct3(types[0..3].*),
else => @compileError("Unsupported length"),
};
}
fn otherMatch(args: anytype) MatcherStructN(args) {
return @call(.auto, MatcherStructN(args).case, args);
}
pub fn main() !void {
const a: EnumA = .Red;
const b: EnumB = .Down;
const s = switch (otherMatch(.{a, b})) {
.case(.Red, .Up) => "red up",
.case(.Red, .Down) => "red down",
.case(.Black, .Up) => "black up",
.case(.Black, .Down) => "black down",
};
std.debug.print("{s}\n", .{s});
}