Errors as values, tests as declarations
Faber keeps failure in the type system instead of in an exception runtime. Three ideas that many languages collapse into one are written differently:
| Construct | Meaning |
|---|---|
→ T | the normal success channel |
T ∪ none | absence inside the success value domain |
⇥ E | a recoverable alternate exit — the error channel |
A function that can fail declares the second channel after the arrow. It does
not return a wrapper type the caller must unwrap; it returns its value by →
and throws by ⇥. throw sends a value on the channel, and the guards
require / reject are the one-line forms: require cond throw err throws
when the condition fails, and reject is its boolean opposite. Callers recover
locally with a do / catch pair, so a failure stays visible at the call site
rather than travelling up an invisible stack.
What it looks like#
A fallible division, and two callers — one that succeeds, one that catches:
fn divide(int a, int b) → int ⇥ string {
require b ≠ 0 throw "division by zero"
return a / b
}
main {
do {
print divide(10, 2)
}
catch err {
print err
}
do {
print divide(1, 0)
}
catch err {
print err
}
}$ faber run
5
division by zeroNothing throws past the caller: the error value binds as err and the program
keeps going. The compiler knows which call sites can fail, because the ⇥
channel is part of the function's type.
Tests are declarations#
The same "make it explicit" stance applies to tests. There is no separate test
binary and no test module tree. Three keywords declare tests in the same file
as the code — or in colocated *.proba sources — and faber test runs them on
the MIR stepper, with no Cargo or rustc involved:
| Keyword | Role |
|---|---|
describe | a named test suite, nestable |
test | one test case |
assert | an assertion that must hold |
fn saturate(int x) → int {
if x ≺ 0 then return 0
if x ≻ 255 then return 255
return x
}
describe "saturate" {
test "clamps low" {
assert saturate(-1) ≡ 0
}
test "clamps high" {
assert saturate(300) ≡ 255
}
}$ faber test .
ok src/main.fab::saturate/clamps low (0 ms)
ok src/main.fab::saturate/clamps high (0 ms)
test result: ok. 2 passed; 0 failed; 0 blocked; 0 skipped(The transcript shortens each case's path to the package root.)
Tests are type-checked and analysed by the same front end as production code, and the test blocks are filtered out of a production build. Because the runner is the stepper, a suite is target-neutral: the same tests run whatever backend the package is emitted for.
The full treatment — async error channels, inline conversion defaults, the
faber test flags — is in Errors and testing.