Outcome
What a fallible call hands back is an outcome: one value that is either a success or a failure. So far every outcome was matched the moment it arrived, but nothing requires that. An outcome is an ordinary value. It can sit in a variable, be passed to a function, and be looked at later — and you can even build one by hand.
Store first, look later
Each call's outcome is stored first and examined afterwards:
let party = Share(12, 4);
let empty = Share(12, 0);
Describe(party);
Describe(empty);
Describe takes the outcome as a parameter like any other value. Its type is written exactly as a return type would be:
func Describe(outcome: int ! NobodyToShare) {
Patterns look inside
.Success(...) and .Failure(...) are the two shapes an outcome can have. In a match they are patterns, and whatever is written inside the parentheses is matched against the payload — a name binds it, a literal compares it, _ ignores it:
match outcome {
.Success(0) => PrintLine("not even one slice each"),
.Success(each) => PrintLine("{} slices each", each),
.Failure(error) => PrintLine("{} slices and nobody to eat them", error.slices)
}
| Pattern | Matches | Binds |
|---|---|---|
.Success(0) | a success whose value is 0 | nothing |
.Success(each) | any success | the value |
.Failure(error) | any failure | the error |
.Failure(_) | any failure | nothing |
Order matters, as in every match: .Success(0) comes before .Success(each), because the general arm would otherwise take the zero too. The compiler catches that order and calls the specific arm unreachable.
Building an outcome by hand
Outside a match, the same two shapes are constructors. No function needs to be called:
let promised: int ! NobodyToShare = .Success(2);
let refused: int ! NobodyToShare = .Failure(NobodyToShare { slices: 8 });
The type annotation is required. .Success(2) says which channel the value goes in, but not what the other channel would have held — and an outcome's type needs both.
A match that produces a value
A match on an outcome can be an expression, with one value per shape:
let eaten = match party {
.Success(each) => each * 4,
.Failure(_) => 0
};
Twelve slices shared among four is three each, so four people eat 12. A match on an outcome must cover both shapes — a failure can never slip through unnoticed.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// What a fallible call hands back is an outcome: one value that is either a success or a
// failure. It is an ordinary value. It can sit in a variable, be passed to a function, and be
// looked at later.
//
// `.Success(...)` and `.Failure(...)` are the two shapes an outcome can have. In a `match` they
// are patterns, and whatever is written inside the parentheses is matched against the payload —
// a name binds it, a literal compares it. Outside a match they are constructors, so an outcome
// can also be built by hand, without calling anything.
import Io::PrintLine;
struct NobodyToShare {
slices: int;
}
// Shares out pizza slices evenly. It fails when there is nobody to share them with.
func Share(slices: int, people: int) -> int ! NobodyToShare {
if people == 0 {
fail NobodyToShare { slices: slices };
}
return slices / people;
}
// An outcome arrives as a parameter like any other value.
func Describe(outcome: int ! NobodyToShare) {
match outcome {
.Success(0) => PrintLine("not even one slice each"),
.Success(each) => PrintLine("{} slices each", each),
.Failure(error) => PrintLine("{} slices and nobody to eat them", error.slices)
}
}
func Main() -> int {
// Each call's outcome is stored first and examined afterwards.
let party = Share(12, 4);
let empty = Share(12, 0);
Describe(party);
Describe(empty);
Describe(Share(3, 4));
// The same two shapes as constructors. The type annotation says which fallible is meant.
let promised: int ! NobodyToShare = .Success(2);
let refused: int ! NobodyToShare = .Failure(NobodyToShare { slices: 8 });
Describe(promised);
Describe(refused);
// A match can also produce a value, one per shape.
let eaten = match party {
.Success(each) => each * 4,
.Failure(_) => 0
};
PrintLine("eaten at the party: {}", eaten);
// A match on an outcome must cover both shapes. Drop the `.Failure` arm above and the
// compiler refuses: "match on 'int ! NobodyToShare' is not exhaustive; missing .Failure(_)".
// A failure can never slip through unnoticed.
return 0;
}
Run it
cd Examples/Errors/Outcome
rux run
3 slices each
12 slices and nobody to eat them
not even one slice each
2 slices each
8 slices and nobody to eat them
eaten at the party: 12
Common mistakes
Drop the
.Failure arm and the compiler refuses: error: match on 'int ! NobodyToShare' is not exhaustive; missing .Failure(_).let promised = .Success(2); is error: cannot infer the type of 'promised' from a native constructor with an unknown channel. Annotate the variable, as the help line shows: let value: int32 ! ParseError = .Success(1i32);.Swap the two success arms and
.Success(0) is rejected: error: match arm is unreachable because earlier arms already match every value it matches. Put specific patterns before general ones.Try it yourself
- Add an arm that prints
"exactly one slice each". Where in the match must it go? - Build an outcome by hand with
.Failure(NobodyToShare { slices: 0 })and pass it toDescribe. - Store
Share(7, 2)in a variable, then use onematchexpression to work out how many slices are eaten and another to print it.
Learn more
- Match expression — a
matchthat produces a value - Catch — a shorter way to deal with only the failure
- Generic outcome — functions that accept any
T ! E