Error mapping
A low-level function knows what went wrong but not where. ParseNumber can say "the character at position 2 is not a digit", but it has no idea that its text came from line 3 of a settings file. The function that does know is the one calling it.
This lesson shows how that caller adds what it knows to the error as it passes through, with ? else (e => ...).
Plain ? cannot add context
Two error types meet here. ParseNumber fails with a DigitError, which knows a position; the settings reader fails with a SettingError, which needs a line and a column:
struct DigitError {
position: uint;
}
struct SettingError {
line: uint;
column: uint;
}
Plain ? passes an error on unchanged, so it cannot add the line number — and it is rejected here anyway, because a DigitError is not a SettingError. The compiler's note is worth reading: '?' moves an error into the outer failure only by identity, sum member injection, or subset widening; it never converts an error.
? else maps the error
The mapped form fixes both problems:
let w = ParseNumber(width)? else (e => SettingError { line: 1, column: e.position + 1 });
let h = ParseNumber(height)? else (e => SettingError { line: 2, column: e.position + 1 });
| Piece | Meaning |
|---|---|
ParseNumber(width)? | on success, the number; the mapper never runs |
else (e => ...) | on failure, e is the whole original DigitError |
SettingError { ... } | the new error, built from e and anything else in scope; the function fails with it |
flowchart LR
call["ParseNumber(width)"] --> q{"Success or failure?"}
q -- "success" --> n["the number;<br/>the mapper never runs"]
q -- "failure, as e" --> m["the mapper builds<br/>SettingError { line: 1, column: e.position + 1 }"]
m --> f["WindowArea fails<br/>with the new error"]The mapper uses what is in scope
The mapper is a single expression, and it can read anything the function can. Here it adds the line number, which only WindowArea knows, and turns the 0-based position into the 1-based column a person would look for. In "8O" the letter O sits at position 1, so the message says column 2.
The parentheses around e => ... hold exactly one name and one expression. The mapper must produce the enclosing function's error type — the compiler checks that, but not whether the arithmetic inside is right.
Where to map
Map an error where the context is. ParseNumber stays small and reusable because it knows nothing about files; WindowArea is the first function that knows about lines, so it is the one that maps. Its own caller then deals with SettingError alone and never needs to know that a DigitError existed.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A low-level function knows what went wrong but not where. `ParseNumber` below
// can say "the character at position 2 is not a digit", but it has no idea that
// its text came from line 3 of a settings file. The function that *does* know
// is the one calling it.
//
// Plain `?` passes an error on unchanged, so it cannot add that context — and
// it is rejected here anyway, because a `DigitError` is not a `SettingError`.
// The mapped form fixes both:
//
// ParseNumber(text)? else (e => SettingError { ... })
//
// On success the expression is the number and the mapper never runs. On
// failure `e` is the whole original error, the mapper builds the new one from
// `e` plus anything else in scope, and the function fails with it.
import Io::PrintLine;
struct DigitError {
position: uint;
}
struct SettingError {
line: uint;
column: uint;
}
// Knows about characters, nothing about files or lines.
func ParseNumber(text: char8[..]) -> int ! DigitError {
var total = 0;
for i in 0..text.length {
let byte = text[i];
if byte < c8'0' || byte > c8'9' {
fail DigitError { position: i };
}
total = total * 10 + ((byte as int) - 48);
}
return total;
}
// Reads a window size from two settings lines. Each `? else` adds the line
// number, which only this function knows, and turns the 0-based position into
// the 1-based column a person would look for.
func WindowArea(width: char8[..], height: char8[..]) -> int ! SettingError {
let w = ParseNumber(width)? else (e => SettingError { line: 1, column: e.position + 1 });
let h = ParseNumber(height)? else (e => SettingError { line: 2, column: e.position + 1 });
return w * h;
}
func Show(width: char8[..], height: char8[..]) {
match WindowArea(width, height) {
.Success(area) => PrintLine("{} x {}: area {}", width, height, area),
.Failure(e) => PrintLine("{} x {}: line {}, column {} is not a digit",
width, height, e.line, e.column)
}
}
func Main() -> int {
Show("80", "24");
Show("8O", "24");
Show("80", "2-4");
return 0;
}
Run it
cd Examples/Errors/ErrorMapping
rux run
80 x 24: area 1920
8O x 24: line 1, column 2 is not a digit
80 x 2-4: line 2, column 2 is not a digit
Common mistakes
let w = ParseNumber(width)?; is error: '?' propagates error type 'DigitError', but the enclosing function fails with 'SettingError', with the help map the error to 'SettingError' with '? else (e => ...)', or match the value.ParseNumber(width)? else (e => 5) is error: the mapped error has type 'int', but the enclosing function fails with 'SettingError'.Write
column: e.position and the program still compiles — it just reports column 1 for "8O". The compiler checks the mapper's type, not its meaning.Try it yourself
- Add a
found: char8field toSettingErrorand fill it in the mapper with the bad character,width[e.position]. Print it inShow. - Call
Show("", "24"). Predict the output before you run it — is an empty line an error? - Read a third setting, a depth on line 3, and print the volume instead of the area.
Learn more
- Propagate — plain
?, which passes an error on unchanged - Error variant — a richer target type for a mapper
- Error sum — passing on two error types without mapping either