Optionals and errors
?T may be absent, !T may have failed, and !T says which failures it means.
?T is a value that may be absent. !T is one that may have failed. Both compile
to a tag next to the payload, in registers, with no allocation and no boxing.
// Optionals and error unions.
//
// `?T` is a value that may be absent; `!T` is one that may have failed. Both
// compile to a tag next to the payload -- no allocation, no boxing.
//
// A plain value coerces into either when the context asks for it, which is what
// lets `return n;` sit in a function declared `!i64`.
fn half(n: i64) ?i64 {
if (n % 2 != 0) { return null; }
return n / 2;
}
fn checked_div(a: i64, b: i64) !i64 {
if (b == 0) { return error.DivideByZero; }
return a / b;
}
// `try` unwraps an error union or returns the error from this function, so
// this one has to be fallible too.
fn average(a: i64, b: i64, count: i64) !i64 {
const total = a + b;
const mean = try checked_div(total, count);
return mean;
}
fn main() i64 {
// `orelse` supplies a value for the null case.
print(half(10) orelse -1);
print(half(7) orelse -1);
// `|v|` binds the payload when it is present.
if (half(8)) |v| { print(v); } else { print("odd"); }
if (half(9)) |v| { print(v); } else { print("odd"); }
// `.?` asserts presence, and aborts if it is wrong.
print(half(6).?);
// `catch` supplies a value for the error case, optionally binding the error.
print(checked_div(10, 2) catch 0);
print(checked_div(10, 0) catch 0);
print(checked_div(10, 0) catch |e| -1);
// The error propagated by `try` surfaces here.
print(average(3, 7, 2) catch 0);
print(average(3, 7, 0) catch -99);
return 0;
}wsharp run examples/results.ws5
-1
4
odd
3
5
0
-1
5
-99Optionals
?i64 is either an i64 or null. Four things act on one:
a orelse b | the payload, or b if it is absent |
if (a) |v| { } | binds the payload when present, with an optional else |
while (a) |v| { } | the same, as a loop, which is how iterators end |
a.? | the payload, and a panic if there is not one |
.? is an assertion. It is the right thing to write when absence would be a bug
rather than a case, and it fails loudly rather than quietly: the process prints
W# panic: unwrapped a null optional and exits 101.
Error unions carry a set
!i64 is either an i64 or an error. What makes it worth more than a boolean is
that the type records which errors:
fn risky(n: i64) !i64 { // inferred: fn(i64) !{Negative, Zero}i64
if (n < 0) { return error.Negative; }
if (n == 0) { return error.Zero; }
return n;
}The set is inferred from what the function raises and from what it propagates
with try. You can also write it down, in which case it is checked:
fn risky(n: i64) !{Negative, Zero}i64 { ... } // exactly these two
fn risky(n: i64) !{Negative}i64 { ... } // error: Zero escapesRaising an error outside a declared set is a compile error. Nothing caps how many errors a set may name.
Because the set is in the type, the e bound by catch |e| is worth testing
against:
const v = risky(n) catch |e| if (e == error.Negative) 0 else -1;Handling one
try f() | the payload, or return this error from the current function |
f() catch 0 | the payload, or 0 |
f() catch |e| ... | the payload, or an expression with the error bound |
f() catch |e| return e | the payload, or hand this error on |
f() catch return false | catch takes any expression, including a return |
f() catch { log(); 0 } | including a block, whose last expression is its value |
try propagates, so a function that uses it has to be fallible itself. That is
why average in the example is declared !i64: it calls checked_div with
try, and checked_div can fail.
A caught error can be handed on. catch |e| ... return e is how a function
handles one case and passes the rest along, and it is held to the same declared set
try is. An error value and an !T share their first slot, so this is one move
rather than a constructor.
Coercion into either
A plain value coerces into an optional or an error union when the context wants
one, so return n; is legal in a function declared !i64 and in one declared
?i64. You never write a constructor.
The same rule covers subtyping: a subtype coerces into its supertype, because both are one pointer and a subtype’s layout begins with a byte-identical copy of its supertype’s. That is the next page.
The steps compose, so return Sub{ .. }; is legal in a function declared
!Base, and !?T is a type worth writing: a value, nothing, or a failure, in one
return.
What panics rather than returning an error
W# separates errors, which are values the type system makes you handle, from
failures the type system permits but the program must not perform. The second
kind panics: it prints W# panic: <reason> to stderr and exits 101.
.?on a null optional- a failed
assert - an array index outside the array
- integer division by zero, and signed
MIN / -1 - a call no overload matches
Ordinary arithmetic overflow is not on that list. +, - and * wrap, which for
an unsigned type is the definition rather than a concession.