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

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