Generic outcome
Some questions get asked of outcomes again and again: "the value, or this fallback", "did it work?", "what went wrong, if anything?". For a struct you would answer them with methods. Optionals, fallibles and sums are different: they are built into the language rather than declared by a package, so there is no package to own methods on them, and extend refuses them. The answer is a generic function whose parameter spells the form out — T ! E or T? — and which therefore works for every success type and every error type at once.
A parameter that spells out the form
ValueOr takes any fallible and a fallback of its success type:
// The success, or `fallback` when there is none.
func ValueOr<T, E>(outcome: T ! E, fallback: T) -> T {
return outcome catch { else => fallback };
}
Nothing is written at the call. Given Divide(12, 4), an int32 ! DivisionByZero, the compiler reads T = int32 and E = DivisionByZero straight out of the argument's type, the way it read T out of a Pair<T> in Generic type. The body is the catch fallback you already know; the only new thing is that it is written once for every fallible there will ever be.
| Parameter written | Accepts | Helpers in this lesson |
|---|---|---|
T ! E | any fallible with a success value | ValueOr, SuccessOf, ErrorOf |
T? | any optional | Both |
T?[..] | a slice of optionals of one type | CountPresent |
Turning one form into another
Two small helpers move between the forms. SuccessOf keeps the success and forgets why there might not be one; ErrorOf keeps the other channel:
func SuccessOf<T, E>(outcome: T ! E) -> T? {
return match outcome {
.Success(value) => .Some(value),
.Failure(_) => none
};
}
func ErrorOf<T, E>(outcome: T ! E) -> E? {
return match outcome {
.Success(_) => none,
.Failure(error) => .Some(error)
};
}
flowchart LR
f(["T ! E"]) -- "SuccessOf" --> s(["T?"])
f -- "ErrorOf" --> e(["E?"])
f -- "ValueOr(fallback)" --> t(["T"])
f -- "Succeeded / Failed" --> b(["bool"])ErrorOf(broken) is a DivisionByZero?, so Main can open it with a presence pattern and read the error's own field:
match ErrorOf(broken) {
error? => PrintLine("ErrorOf cannot divide {} by zero", error.numerator),
none => PrintLine("ErrorOf nothing went wrong")
}
Core ships two of these
The two questions everybody asks are already in Core, written exactly this way. Succeeded(outcome) and Failed(outcome) answer which channel an outcome holds, without unwrapping it:
import Core::{ Failed, Succeeded };
Their signatures are Succeeded<T, E>(outcome: T ! E) -> bool and the same for Failed — generic functions over a fallible, like the ones in this lesson.
Optionals too
Both combines two optionals of possibly different types. The postfix ? from Optional propagate does the work: if either is none, the function returns none on the spot.
func Both<T, U>(first: T?, second: U?) -> (T, U)? {
return (first?, second?);
}
And a helper can take a whole slice of optionals. T?[..] is a slice of T?, so an int32?[4] array is passed straight to it, and is asks whether each one holds a T:
func CountPresent<T>(values: T?[..]) -> uint {
var count: uint = 0;
for value in values {
if value is T {
count++;
}
}
return count;
}
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Optionals, fallibles and sums are built into the language rather than declared by a package,
// so there is no package to own methods on them. `extend int32 ! DivisionByZero { ... }` is
// rejected: "cannot extend native type 'int32 ! DivisionByZero'". A question you keep asking of
// an outcome is written instead as a generic function whose parameter spells the form out:
// `T ! E`, or `T?`.
//
// Such a helper works for every success type and every error type at once, and nothing needs
// to be written at the call: `T` and `E` are read straight out of the argument's type.
//
// `Core` ships the two everybody needs, written exactly this way: `Succeeded(outcome)` and
// `Failed(outcome)` answer which channel an outcome holds without unwrapping it.
import Core::{ Failed, Succeeded };
import Io::PrintLine;
struct DivisionByZero {
numerator: int32;
}
func Divide(numerator: int32, denominator: int32) -> int32 ! DivisionByZero {
if denominator == 0 {
fail DivisionByZero { numerator: numerator };
}
return numerator / denominator;
}
// The success, or `fallback` when there is none.
func ValueOr<T, E>(outcome: T ! E, fallback: T) -> T {
return outcome catch { else => fallback };
}
// Keeps the success and forgets why there might not be one.
func SuccessOf<T, E>(outcome: T ! E) -> T? {
return match outcome {
.Success(value) => .Some(value),
.Failure(_) => none
};
}
// The other channel: the error, if there is one.
func ErrorOf<T, E>(outcome: T ! E) -> E? {
return match outcome {
.Success(_) => none,
.Failure(error) => .Some(error)
};
}
// Optionals too: both values when both are present, and nothing otherwise.
func Both<T, U>(first: T?, second: U?) -> (T, U)? {
return (first?, second?);
}
// And a whole array of them: an `int32?[4]` is passed straight to the `T?[..]` slice.
func CountPresent<T>(values: T?[..]) -> uint {
var count: uint = 0;
for value in values {
if value is T {
count++;
}
}
return count;
}
func Main() -> int {
let even = Divide(12, 4);
let broken = Divide(7, 0);
PrintLine("ValueOr {} {}", ValueOr(even, -1), ValueOr(broken, -1));
PrintLine("Succeeded {} {}", Succeeded(even), Succeeded(broken));
PrintLine("Failed {} {}", Failed(even), Failed(broken));
PrintLine("SuccessOf {}", SuccessOf(even) ?? 0);
match ErrorOf(broken) {
error? => PrintLine("ErrorOf cannot divide {} by zero", error.numerator),
none => PrintLine("ErrorOf nothing went wrong")
}
let width: int32? = 80;
let height: int32? = 24;
let missing: char8[..]? = none;
match Both(width, height) {
size? => PrintLine("Both {} x {}", size.0, size.1),
none => PrintLine("Both incomplete")
}
PrintLine("Both complete: {}", Both(width, missing) is (int32, char8[..]));
let readings: int32?[4] = [3, none, 5, none];
PrintLine("CountPresent {} of {}", CountPresent(readings), readings.length);
return 0;
}
Besides Io, its Rux.toml lists Core under [Dependencies].
Run it
cd Examples/Generics/GenericOutcome
rux run
ValueOr 3 -1
Succeeded true false
Failed false true
SuccessOf 3
ErrorOf cannot divide 7 by zero
Both 80 x 24
Both complete: false
CountPresent 2 of 4
Common mistakes
extend int32 ! DivisionByZero { … } fails with error: cannot extend native type 'int32 ! DivisionByZero', and extend int32? { … } with cannot extend native type 'int32?'. The note explains why — a sum, optional, fallible, or unit type has no declaring package to own methods or interface implementations — and the help gives this lesson's answer: write a generic function that takes the native type as a parameter.ValueOr(12, -1) fails with error: function 'ValueOr' requires 2 type arguments, but 0 were provided. A plain int has no error type, so there is nothing to read E from. Only a real T ! E fills in both parameters.Try it yourself
- Write
OrElse<T>(value: T?, fallback: T) -> Twith??, and call it with anint32?and achar8[..]?. - Write
IsPresent<T>(value: T?) -> bool, then rewriteCountPresentto use it. - Write
Either<T, E>(first: T ! E, second: T ! E) -> T ! Ethat returnsfirstif it succeeded andsecondotherwise. - Use
ValueOron the result of a function from an earlier lesson that returns a different fallible. Did you have to changeValueOr?
Learn more
- Outcome and Catch fallback — the forms these helpers take apart
- Optional propagate and Is — the tools inside
BothandCountPresent - Error handling in the Rux Reference
- Generic sum — the third native form,
A | B, with type parameters