Errors · Lesson 9.3

Unit fallible

Source
Write a fallible function with no success value, -> ! E, which succeeds by reaching its end.
You'll need: Fail, Mutable reference

Some operations have no answer to give back. When a withdrawal works, the money is gone from the account and there is nothing more to say. But it can still fail, and the caller still needs to know.

Such a function is written -> ! E: an error type after the !, and nothing before it.

! E is () ! E

! E is short for () ! E. The () is the unit — the empty tuple, whose only value is also written (). It is a success that carries no information beyond "it worked". Both spellings compile and mean the same type; the short one is what you will see everywhere:

func Withdraw(account: &var Account, amount: int) -> ! ShortOfFunds {

The account is a mutable reference, so the function changes the caller's account in place. The only thing left to report is whether that happened.

Three ways out

Because there is no value to return, the function succeeds simply by reaching its end. A bare return; succeeds early, and only fail makes it fail:

if amount == 0 {
    // Nothing to do, and that counts as success.
    return;
}
if amount > account.balance {
    fail ShortOfFunds { missing: amount - account.balance };
}
account.balance -= amount;
// Falling off the end: success.
The function…The outcome
reaches its closing bracesuccess
runs return;success, early
runs fail error;failure, carrying error

Matching a unit success

The success channel still holds a value — the unit — so its pattern has parentheses with () inside:

match Withdraw(account, amount) {
    .Success(()) => PrintLine("take {}: done, {} left", amount, account.balance),
    .Failure(error) => PrintLine("take {}: refused, {} short", amount, error.missing)
}

.Success(_) works too, since _ matches any value, the unit included. What does not work is leaving the parentheses out: .Success alone is a pattern with no payload, and the success channel always has one.

The output follows the balance as it changes. Taking 0 succeeds without touching anything, 80 is refused while 60 is left, and the next 60 empties the account — so even 1 is refused after that.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Some operations have no answer to give back. When a withdrawal works, the money is gone from
// the account and there is nothing more to say. Such a function is written `-> ! E`: an error
// type after the `!`, and nothing before it.
//
// `! E` is short for `() ! E`. The `()` is the unit, the empty tuple, whose only value is also
// `()` — a success that carries no information beyond "it worked". Because there is no value to
// return, the function succeeds simply by reaching its end. A bare `return;` succeeds early, and
// only `fail` makes it fail.
import Io::PrintLine;

struct Account {
    balance: int;
}

struct ShortOfFunds {
    missing: int;
}

// Changes the account in place; the only thing to report is whether that happened.
func Withdraw(account: &var Account, amount: int) -> ! ShortOfFunds {
    if amount == 0 {
        // Nothing to do, and that counts as success.
        return;
    }
    if amount > account.balance {
        fail ShortOfFunds { missing: amount - account.balance };
    }
    account.balance -= amount;
    // Falling off the end: success.
}

func Attempt(account: &var Account, amount: int) {
    // The success pattern holds the unit, so it is written `.Success(())`.
    match Withdraw(account, amount) {
        .Success(()) => PrintLine("take {}: done, {} left", amount, account.balance),
        .Failure(error) => PrintLine("take {}: refused, {} short", amount, error.missing)
    }
}

func Main() -> int {
    var account = Account { balance: 100 };
    Attempt(account, 40);
    Attempt(account, 0);
    Attempt(account, 80);
    Attempt(account, 60);
    Attempt(account, 1);
    return 0;
}

Run it

cd Examples/Errors/UnitFallible
rux run
take 40: done, 60 left
take 0: done, 60 left
take 80: refused, 20 short
take 60: done, 0 left
take 1: refused, 1 short

Common mistakes

Returning a value from a ! E function.
return 0; inside Withdraw is error: 'return' value must have type '! ShortOfFunds', but found 'int'. There is no success value to give; write return; or let the function reach its end.
Writing .Success without the unit.
.Success => ... is refused with error: pattern '.Success' expects 1 field, but found 0. Write .Success(()) or .Success(_).
Calling it as a plain statement.
Withdraw(account, 40); on its own is error: fallible result of type '! ShortOfFunds' is discarded. No value comes back, but the failure still does. Discard explains how to ignore it on purpose.

Try it yourself

  1. Change the signature to the long spelling -> () ! ShortOfFunds and check that the program still compiles and prints the same.
  2. Add Deposit(account: &var Account, amount: int) -> ! TooMuch that refuses when the balance would pass 1000.
  3. Replace .Success(()) with .Success(_). Does anything change?

Learn more

  • Fail — the failure channel's return
  • Discard — the deliberate way to ignore a ! E result
  • Tuple — the empty tuple () is the unit
  • Mutable reference — how Withdraw changes the caller's account