I see it more like a double-unwrapping, so !?T
reads as T
, ?
, !
.
My reasoning is this. Right now, what you get is the following:
fn optionErr(i: usize) !?f64 {
switch (i) {
0 => return 1.5,
1 => return null,
else => return error.NotBinary,
}
}
test "optional error" {
const val: usize = 0; // set to whatever
if (optionErr(val)) |cap| {
if (cap) |inner_cap| {
std.debug.print("Got a float {d}\n", .{inner_cap});
} else {
std.debug.print("It's a null\n", .{});
}
} else |err| {
std.debug.print("Got an error {!}\n", .{err});
}
}
So a “double else” just unwraps the inner conditional:
test "optional error" {
const val: usize = 0; // set to whatever
if (optionErr(val)) |cap| {
std.debug.print("Got a float {d}\n", .{inner_cap});
} else {
std.debug.print("It's a null\n", .{});
} else |err| {
std.debug.print("Got an error {!}\n", .{err});
}
}
But the complexity of this is evidenced by you and I seeing different interpretations as more natural.
The saving grace of the idea is that, like the three existing varieties of if
statement, the correct form for a given type would be mandatory. Whether the error comes first or second, it requires two else clauses, and one of them has no capture, because it’s null.
Unless there’s a leading !
in the conditional, of course:
test "optional error reversed" {
const val: usize = 0; // set to whatever
if (!optionErr(val)) {
std.debug.print("It's a null\n", .{});
} else |cap| {
std.debug.print("Got a float {d}\n", .{inner_cap});
} else |err| {
std.debug.print("Got an error {!}\n", .{err});
}
}
I think that one would be more confusing if the error-capturing else
came before the null else
, because logically, that would put the intended value all the way at the bottom.
But the cool thing about all of them is that you don’t need any type or semantic analysis at all to know what kind of if statement you’re dealing with, it’s built in to the parse. Whether it’s correct depends on the types, of course, but that’s true in general.