Errors · Lesson 9.7

Catch fallback

Source
Use catch { else => fallback } to get the success or a default value.
You'll need: Catch, Coalesce

Often the reason for a failure does not matter, only that there is a value to carry on with. outcome catch { else => fallback } says exactly that: the success if there is one, otherwise the fallback. The single else arm takes every error alike, so nothing needs listing.

One arm for every error

A settings form where every field is one typed digit, each with a sensible default:

let volume = ReadDigit('7') catch { else => 5 };
let brightness = ReadDigit('x') catch { else => 8 };
let contrast = ReadDigit(' ') catch { else => 5 };

'7' reads fine, so volume is 7. 'x' is not a digit and ' ' is blank — two different errors, and both get the field's default. The fallback must have the success type, an int here.

The twin of ??

If you have done Coalesce, the shape is familiar. catch { else => ... } is the fallible twin of option ?? fallback:

You haveValue or defaultThe fallback runs
T?option ?? fallbackonly when absent
T ! Eoutcome catch { else => fallback }only on a failure

Like ??, the fallback is worked out only when it is needed. A fallback that calls a function which prints something prints nothing at all when the read succeeds.

Why not just ?? on a fallible, then? Because ?? would drop the error without a word, and the compiler will not do that silently. On a fallible it says operator '??' cannot take 'int ! DigitError', and the note explains: coalescing tests one optional level and would silently discard an error. Writing catch { else => ... } is how you say that dropping it is intended.

catch binds tightly

catch binds tightly to the call right before it, so several of them can sit in one expression without parentheses. Adding up a code, where a digit that cannot be read counts as nothing:

let total = ReadDigit('4') catch { else => 0 } + ReadDigit('?') catch { else => 0 }
    + ReadDigit('9') catch { else => 0 };

Each catch belongs to the ReadDigit just before it, so the sum is 4 + 0 + 9 = 13.

The price

The convenience has a price: the error is thrown away unseen. 'x' and ' ' both became defaults, and nothing in the program can tell which went wrong or why. When the reason should change what happens next, give catch one arm per case instead, as Catch does.

The program

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

Src/Main.rux
// Often the reason for a failure does not matter, only that there is a value to carry on with.
// `outcome catch { else => fallback }` says exactly that: the success if there is one, otherwise
// the fallback. The single `else` arm takes every error alike, so nothing needs listing.
//
// It is the fallible twin of `option ?? fallback`. The fallback is only worked out when it is
// needed, and it must have the success type — an `int` here.
//
// `catch` binds tightly to the call right before it, so several of them can sit in one
// expression without parentheses.
import Io::PrintLine;

variant DigitError {
    Blank,
    NotADigit(char)
}

func ReadDigit(c: char) -> int ! DigitError {
    if c == ' ' {
        fail DigitError::Blank;
    }
    if c < '0' || c > '9' {
        fail DigitError::NotADigit(c);
    }
    return (c as int) - ('0' as int);
}

func Main() -> int {
    // A settings form where every field is one typed digit, each with a sensible default.
    let volume = ReadDigit('7') catch { else => 5 };
    let brightness = ReadDigit('x') catch { else => 8 };
    let contrast = ReadDigit(' ') catch { else => 5 };
    PrintLine("volume {}, brightness {}, contrast {}", volume, brightness, contrast);

    // Adding up a code, where a digit that cannot be read counts as nothing.
    let total = ReadDigit('4') catch { else => 0 } + ReadDigit('?') catch { else => 0 }
        + ReadDigit('9') catch { else => 0 };
    PrintLine("sum of 4?9 is {}", total);

    // The convenience has a price: the error is thrown away unseen. When the reason should
    // change what happens next, give `catch` one arm per case instead.
    return 0;
}

Run it

cd Examples/Errors/CatchFallback
rux run
volume 7, brightness 8, contrast 5
sum of 4?9 is 13

Common mistakes

Using ?? on a fallible.
ReadDigit('7') ?? 5 is refused: error: operator '??' cannot take 'int ! DigitError'. The help line names the two ways out: recover the error with 'catch', or propagate it with '?'.
A fallback of the wrong type.
catch { else => 8.5 } is error: 'catch' arm produces 'float64', but the recovered value has type 'int'. The fallback stands in for the success, so it must be an int.

Try it yourself

  1. Add up the digits '7', 'x' and '7' with a fallback of 0, then with a fallback of 5. Predict both sums.
  2. Write func Default() -> int that prints "(using the default)" and returns 5, and use it as the fallback for volume. When does the line appear?
  3. Rewrite brightness with one arm per case — Blank gives 8, NotADigit(_) gives 0.

Learn more

  • Catch — one arm per error case
  • Coalesce — the same idea for optionals
  • Propagate — when the caller should decide instead