Errors · Lesson 9.4

Outcome

Source
Store, pass, build and match a fallible's outcome through .Success and .Failure.
You'll need: Fail, Match expression

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)
}
PatternMatchesBinds
.Success(0)a success whose value is 0nothing
.Success(each)any successthe value
.Failure(error)any failurethe error
.Failure(_)any failurenothing

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.

Src/Main.rux
// 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

Forgetting the failure arm.
Drop the .Failure arm and the compiler refuses: error: match on 'int ! NobodyToShare' is not exhaustive; missing .Failure(_).
A constructor with nothing to say what it builds.
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);.
The general arm first.
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

  1. Add an arm that prints "exactly one slice each". Where in the match must it go?
  2. Build an outcome by hand with .Failure(NobodyToShare { slices: 0 }) and pass it to Describe.
  3. Store Share(7, 2) in a variable, then use one match expression to work out how many slices are eaten and another to print it.

Learn more