Failures as data¶
A data failure is a value of the program. Its type says it may be a failure, the checker holds every use of it to that type, and the run writes no diagnostic for it.
Result¶
Result<T, E> is ok(v) or err(e). A function that may fail declares it
in its result type, and a match covers both cases:
fn parse(text: String) : Result<Int, String> = match text.to_int() {
some(n) => ok(n),
none => err(fmt("not a number: {}", text)),
}
["4", "x", "15"] | map(parse)
[ok(4), err("not a number: x"), ok(15)]
A Result is not its value. parse("4") + 1 is a type error, found before
anything runs:
parse("4") + 1
check error[type-mismatch]: addition requires two Int, two Float or two Decimal operands, or two values of a type implementing `Add`, not Result<Int, String>
--> <expression>:7:1
|
7 | parse("4") + 1
| ^^^^^^^^^^
unwrap_or gives the value or a default, map_err changes the error, and
oks() and errs() keep one side of a sequence of results:
results = ["4", "x", "15"] | map(parse)
{
total: results | oks() | sum,
rejected: results | errs(),
fallback: parse("x").unwrap_or(0),
}
{fallback: 0, rejected: ["not a number: x"], total: 19}
validate: a contract read as a value¶
x | validate(T) reads x as the declared type T and gives
Result<T, Invalid>. An Invalid carries the contract, the value and one
Violation per broken rule, each with its path, kind, expected and
actual:
type User { name : String, age : Int }
{name: "bob", age: "forty"} | validate(User)
err(Invalid(contract = "User", value = {age: "forty", name: "bob"}, violations = [Violation(contract = "User", path = ["age"], kind = "type", rule = "User.age.type", text = "\"forty\" is not Int", expected = "Int", actual = "forty", severity = "error", message = "field `age`: \"forty\" is not Int")]))
Over many rows, errs() keeps the rejected ones as data the program can
count, sort or write out:
type User { name : String, age : Int }
rows = [{name: "ada", age: 36}, {name: "bob", age: "forty"}]
rows
| map(row => row | validate(User))
| errs()
| map(invalid => invalid.violations | map(v => v.message))
[["field `age`: \"forty\" is not Int"]]
The run above writes no diagnostic, and its exit status is 0. A Violation
with severity = "warning" is data in the same way.
x | T: a contract read as a requirement¶
Without validate, x | T requires x to read as T. A value that does
not is a failure of the execute phase, written as a diagnostic, and the run
ends:
type User { name : String, age : Int }
Data("{\"name\": \"bob\", \"age\": \"forty\"}", Json) | User
execute error[contract-violation]: this does not read as User: field `age`: "forty" is not Int
--> <expression>:3:1
|
3 | Data("{\"name\": \"bob\", \"age\": \"forty\"}", Json) | User
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= expected: User
The diagnostic carries the same Violations validate would have given, as
its context. They remain the diagnostic's: no operation reads them back into
the program. Which of the two forms to write is the program's decision, at the
call site: validate where a rejected value is part of the result, | T
where it ends the run.
What is next¶
Diagnostics describes the other side: what a diagnostic carries, where it goes, and when a run stops.