const Foo = union(enum) {
One: i32,
Two: bool,
Three: i32,
};
fn bar(f: Foo) void {
switch (f) {
Foo.One, Foo.Three => |x| { },
Foo.Two => |b| { },
}
}
It should be possible to omit the Foo prefix. There's no ambiguity.
And if the unioned types are unique, I may prefer to use them:
const Foo = union(enum) {
One: i32,
Two: bool,
Three: u32,
};
fn bar(f: Foo) void {
switch (f) {
u32, i32=> |x| { },
bool => |b| { },
}
}
It should be possible to omit the Foo prefix. There's no ambiguity.
The above behavior works now but there might be a soundness issue here as the following works too:
const Foo = union(enum) {
One: i32,
Two: bool,
Three: i32,
};
fn bar(f: Foo) void {
switch (f) {
.One, .Two, .Three => |x| {
@import("std").debug.warn("{}\n", x);
},
}
}
test "" {
bar(Foo{ .One = 21 });
bar(Foo{ .Two = false });
bar(Foo{ .Three = -1 });
}
Hmm yes that looks like a missing compile error.
The above behavior works now but there might be a soundness issue here as the following works too:
It's not unsound, x is not the payload but f, it's just...weird?
Yeah. The type of x becomes Foo when more than one cases are specified.
xis not the payload butf
This has bitten me before. When I see |x|, I always expect it to unwrap something. So in this case I would not expect x to be the same type as f. Not to mention that the behavior of "|x| unwraps, unless the case's tag types don't match" is pretty gnarly.
then would this not be allowed?
fn bar(f: Foo) void {
switch (f) {
.One => |x| {
@import("std").debug.warn("{}\n", x);
},
// tag types don't match
else => |x| {
@import("std").debug.warn("{}\n", x);
},
}
}
then would this not be allowed?
I would say it should not be allowed, because the else => should have the same effective behavior as .Two, .Three =>.
My intuition is that |x| always unwraps, and to get the original Foo you must use f.
would that be a compiler error then?
Yep, a compiler error would make sense. Something along these lines
error: cannot unwrap union when tag types don't match
note: mismatched types were Foo.two (bool), Foo.three (i32)
on second thought i think the existing behavior is correct for else prongs. it allows for less verbose code like this in the standard library:
switch (kernel32.GetLastError()) {
ERROR.SHARING_VIOLATION => return error.SharingViolation,
ERROR.ALREADY_EXISTS => return error.PathAlreadyExists,
ERROR.FILE_EXISTS => return error.PathAlreadyExists,
ERROR.FILE_NOT_FOUND => return error.FileNotFound,
ERROR.PATH_NOT_FOUND => return error.FileNotFound,
ERROR.ACCESS_DENIED => return error.AccessDenied,
ERROR.PIPE_BUSY => return error.PipeBusy,
ERROR.FILENAME_EXCED_RANGE => return error.NameTooLong,
else => |err| return unexpectedError(err),
}
An alternative, if capturing an else were to be an error:
const err = kernel32.GetLastError(); // store in `e` rather than using `|err|`
switch (err) {
ERROR.SHARING_VIOLATION => return error.SharingViolation,
// ...
else => return unexpectedError(err),
}
It would still be confusing to me if |err| sometimes unwraps and sometimes doesn't, depending on context. Though maybe it's a little better if that inconsistency were isolated only to else conditions.
The fact that else gives a payload which is the same thing (and type) as the target expression is intentional, and @emekoi pointed out above where I used it in the standard library. I'm open to the proposal to reverse this decision, but it's very much intentional in status quo.
I do think it should be a compile error on other switch prongs, however, when they do not share the same type.
Most helpful comment
The fact that
elsegives a payload which is the same thing (and type) as the target expression is intentional, and @emekoi pointed out above where I used it in the standard library. I'm open to the proposal to reverse this decision, but it's very much intentional in status quo.I do think it should be a compile error on other switch prongs, however, when they do not share the same type.