Of course, in my scenario, E is a generic that can add options on top of the default ones. For the default code, it’s enough for other options to be supported in the form of else. What I’m worried about is that if no options are added, using else might cause a compilation error.
Without seeing the real code, I think I still don’t get how the other options work.
Use inline else like this
I still don’t get it. ^_^. How can the else prong match user custom tags?
if E is generic you’re not going to know the tags, you need something that can match any tag --that is not matched by other prongs (if there are other prongs)-- that is exactly what else is…
I hope that makes sense because I don’t know how to explain it any more clearly.
It used to be a compile error, but they fixed it.
A simple example will be very helpful.
fn foo(E: type, e: E) void {
switch (e) {
else => |t| std.log.debug("{}", .{t}),
}
}
the author of foo has no idea what tags the enum has
How will this be useful in the case of @npc1054657282?
const U = union(E) { foo, bar, }; var v: U = .foo; pub fn main() void { // This one also compiles. switch (v) { .foo => {}, .bar => {}, else => {}, } }
with tagged unions you get two captures |payload, tag|, and you should use inline else otherwise it will error if payload types differ per tag.
then you can do whatever logic you want on them, assuming you want to, you can just have a plain else => {} to ignore them.
If U is generated from a generic, the code below knows that U definitely has foo and bar tags, but there’s no way to know if the user will add other tags to it. If else compile error here, that means this code will be broken if the user hasn’t added any tags to U.
As we don’t know what the type of payload and what is name of tag, so do you mean we must pass them to a callback to let users who know them to handle them?
then you can do whatever logic you want on them, assuming you want to, you can just have a plain
else => {}to ignore them.
Or just omit the else prong?
you cant omit it as switches have to be exhaustive.
Do whatever makes sense for your use case
As any extra named tags will make compilation fail. Here, for any tags, do you mean unnamed tags in non-exhaustive enums?
A complete run-able code for @npc1054657282’s case will remove all the confessions. ![]()
unfortunately, @npc1054657282’s desired behaviour of being able to have an else when there are no unhandled tags is not how it currently works, so it’d look something like:
switch (v) {
inline else => |p, t| if (t == .foo) {
std.debug.print("foo", .{});
} else if (t == .bar) {
std.debug.print("bar", .{});
} else {
std.debug.print("other: {}", .{@TypeOf(p)});
},
}
Now if you didn’t need to do anything with the payload then you don’t need a switch, but I assume that isn’t the case.
Sorry, I mis-thought that the switch code block is used in the lib code. It is actually used in user code.
Here is full run-able code of the case of @npc1054657282, in my understanding.
It looks only non-exhaustive enum types work for this case, and the types of custom playload are unknown even in user code. I haven’t got how this case will be useful in practice.
//================== The lib code
pub fn GetU(E: type) type {
return union(E) {
foo: u8,
bar: bool,
};
}
//=================== The user code
const E0 = enum(u3) {
foo,
bar,
baz,
_,
};
const E1 = enum(u3) {
foo,
bar,
_,
};
const E2 = enum(u4) {
foo,
bar,
_,
};
pub fn main() void {
// _ = @as(GetU(E0), .{ .foo = 123 }); // error
const v1 = @as(GetU(E1), .{ .foo = 123 });
switch (v1) {
.foo => {},
.bar => {},
else => {},
}
const v2 = @as(GetU(E2), .{ .foo = 123 });
switch (v2) {
.foo => {},
.bar => {},
else => |tv, t| {
_ = tv; // What is its type? Even the user code doesn't know this.
const n = @intFromEnum(t);
if (n == 2) {
// ...
}
},
}
}
Sorry, I took some time to fix this demo so it could keep up with the latest version on Pastebin
This demo doesn’t fully match the requirements mentioned above because all the options are configured by the user. But imagine a situation where some options are preset by the library and the others are added by the user.
I was wrong, union doesn’t work the same regardless of the exhaustiveness of the tag int, you can have an unused else branch if your tag is non-exhaustive while using all prongs.
const E = enum(u2) {
foo,
bar,
another,
_, // NOTE with this commented out: unreachable else prong; all cases already handled
};
const U = union(E) {
foo,
bar,
another,
};
var v: U = .foo;
pub fn main() void {
// This one also compiles.
switch (v) {
.foo => {},
.bar => {},
.another => {},
else => |T| std.log.debug("{}", .{T}),
}
}
const std = @import("std");
It works when the tag enum is non-exhaustive and doesn’t name every possibility, because if you name each you get the error:
error: non-exhaustive enum specifies every value
The unnamed tag values can’t be used to construct a union value that will work in a switch (according to my experiments, it results in a runtime panic: switch on corrupt value).
var bytes: [@sizeOf(U)]u8 = @splat(3);
const ptr: *const U = @ptrCast(&bytes);
const invalid: U = ptr.*;
// This one compiles, but creates a runtime panic: switch on corrupt value (in safe modes)
switch (invalid) {
.foo => {},
.bar => {},
.another => {},
else => |T| std.log.debug("{}", .{T}),
}
I think this is good, just wanted to test it.
If you have prongs for all named union fields this is dead code.
If you don’t have prongs for all named union fields, the code will run.
The type is accessed via reflection/comptime:
const PayloadType = @FieldType(@TypeOf(v2), @tagName(t));
std.log.debug("PayloadType: {}", .{PayloadType});
You may need to change the else prong into an inline else, to make the tag comptime known. (in cases where you switch on a runtime value).
However the if (n == 2) will be dead code because you can’t switch on union values with a unnamed tag value, because they are ‘corrupt’ values.
If you add another name to the enum you get the error:
error: enum field 'baz' missing from union