Propagate
Handling a fallible with match or catch is right when this function is the one that should decide what a failure means. Often it is not. The function is a step in the middle, and a failure should simply become the caller's problem.
Postfix ? does exactly that. On success, step? is the value inside. On failure, the enclosing function fails at once with that same error, and nothing after the ? runs.
The long way
Written by hand, passing a failure on is the same few lines every time: match, keep the success, fail with the error unchanged.
var high = 0;
match ReadDigit(tens) {
.Success(value) => high = value,
.Failure(error) => fail error
}
TwoDigitsByHand writes that block twice, once per digit. The two blocks are near-identical and say nothing new.
The same with ?
? is those lines written once. The success values are used straight away:
func TwoDigits(tens: char, units: char) -> int ! DigitError {
let high = ReadDigit(tens)?;
let low = ReadDigit(units)?;
return high * 10 + low;
}
flowchart LR
step["ReadDigit(tens)?"] --> q{"Success or failure?"}
q -- ".Success(value)" --> go["the expression is value;<br/>TwoDigits carries on"]
q -- ".Failure(error)" --> out["TwoDigits fails at once<br/>with the same error"]
out --> caller["the caller's match<br/>sees the original error"]Both versions behave identically: ? is shorthand, not a different rule. When both digits are wrong, as in TwoDigits('x', '?'), only the first one is reported — the first ? leaves, and the second read never happens.
The error travels intact
Neither TwoDigits nor ? looked at the error, yet the caller still learns which character was wrong:
.Failure(DigitError::NotADigit(c)) => PrintLine("'{}' is not a digit", c)
Show matches the nested pattern .Failure(DigitError::NotADigit(c)) directly, reaching through the failure channel into the variant case in one step.
Two requirements
? asks two things of the function it is used in:
- It must be fallible, since that is where the failure goes.
Mainreturns a plainint, soReadDigit('4')?there is rejected. - The error must fit its error type. Here both are
DigitError, so it passes through as is. A different error type needs Error mapping, or an error sum that includes it.
| Tool | Who decides what a failure means | The function stays |
|---|---|---|
match, catch | this function | as fallible as you choose |
? | the caller | fallible, with the same error type |
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Handling a fallible with `match` or `catch` is right when this function is the one that should
// decide what a failure means. Often it is not: the function is a step in the middle, and a
// failure should simply become the caller's problem.
//
// Written by hand that is the same few lines every time: match, keep the success, fail with the
// error unchanged. Postfix `?` is those lines written once. On success, `step?` is the value
// inside. On failure, the enclosing function fails at once with that same error, and nothing after
// the `?` runs.
import Io::PrintLine;
variant DigitError {
Blank,
NotADigit(char)
}
// One small step that can fail.
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);
}
// The long way. Each failure is matched only to be handed straight back to the caller: two
// near-identical blocks that say nothing new.
func TwoDigitsByHand(tens: char, units: char) -> int ! DigitError {
var high = 0;
match ReadDigit(tens) {
.Success(value) => high = value,
.Failure(error) => fail error
}
var low = 0;
match ReadDigit(units) {
.Success(value) => low = value,
.Failure(error) => fail error
}
return high * 10 + low;
}
// The same function with `?`. The success values are used straight away.
func TwoDigits(tens: char, units: char) -> int ! DigitError {
let high = ReadDigit(tens)?;
let low = ReadDigit(units)?;
return high * 10 + low;
}
func Show(outcome: int ! DigitError) {
match outcome {
.Success(value) => PrintLine("read {}", value),
.Failure(DigitError::Blank) => PrintLine("a digit is missing"),
.Failure(DigitError::NotADigit(c)) => PrintLine("'{}' is not a digit", c)
}
}
func Main() -> int {
Show(TwoDigits('4', '2'));
Show(TwoDigits('4', 'x'));
Show(TwoDigits('?', '2'));
Show(TwoDigits(' ', '7'));
// Both versions behave identically: `?` is shorthand, not a different rule.
Show(TwoDigitsByHand('4', 'x'));
// The error travels intact. Neither `TwoDigits` nor `?` looked at it, yet the caller still
// learns which character was wrong.
//
// `?` has two requirements. The function using it must itself be fallible, since that is
// where the failure goes. In `Main`, which returns a plain `int`, `ReadDigit('4')?` is
// rejected: "'?' propagates native fallible 'int ! DigitError', but the enclosing function
// returns 'int'". And the error must fit that function's error type. Here both are
// `DigitError`, so it passes through as is.
return 0;
}
Run it
cd Examples/Errors/Propagate
rux run
read 42
'x' is not a digit
'?' is not a digit
a digit is missing
'x' is not a digit
Common mistakes
In
Main, ReadDigit('4')? is error: '?' propagates native fallible 'int ! DigitError', but the enclosing function returns 'int'. Handle the failure with match or catch there — or make Main fallible, as Fallible main shows.let high = ReadDigit(tens); makes high the whole fallible, and the next line fails: error: operator '*' cannot combine left operand 'int ! DigitError' with right operand 'int'.? never converts an error. In a function that fails with a SettingError, ? on a DigitError is error: '?' propagates error type 'DigitError', but the enclosing function fails with 'SettingError'. Error mapping is the fix.Try it yourself
- Add
Show(TwoDigits(' ', 'x'));and predict which of the two messages it prints. - Write
ThreeDigits(hundreds: char, tens: char, units: char) -> int ! DigitErrorwith three?. - Rewrite
ThreeDigitsto callTwoDigitsfor the last two digits. Does the?still need anything special?
Learn more
- Optional propagate — the same
?for absence - Error mapping —
? elseadds context on the way through - Fallible main —
?at the top level of a program - Error variant — one error type for a whole chain of steps