Skip to content

Results

Result<T, E> is a computation that may have failed: ok(v) holding a T, or err(e) holding an E that says how. The possibility of failure is in the type, so a caller cannot forget it, and the error is a value, so a program can say what it is.

Building and matching

ok(v) and err(e) build a result, and ok(p) and err(p) match one. An error is any type, here a record the program declares for it:

type ParseError { input : String, reason : String }

fn parse_age(text: String) : Result<Int, ParseError> = match text.to_int() {
    some(n) given n >= 0 => ok(n),
    some(_) => err(ParseError(input = text, reason = "negative")),
    none => err(ParseError(input = text, reason = "not a number")),
}

["42", "-3", "forty"] | map(parse_age)
[ok(42), err(ParseError(input = "-3", reason = "negative")), err(ParseError(input = "forty", reason = "not a number"))]

A match over a result covers both cases, and a missing one is a type error, as over any union:

type ParseError { input : String, reason : String }

fn parse_age(text: String) : Result<Int, ParseError> = match text.to_int() {
    some(n) given n >= 0 => ok(n),
    some(_) => err(ParseError(input = text, reason = "negative")),
    none => err(ParseError(input = text, reason = "not a number")),
}

fn describe(r: Result<Int, ParseError>) : String = match r {
    ok(n) => fmt("age {}", n),
    err(e) => fmt("cannot read {}: {}", e.input, e.reason),
}

["42", "-3", "forty"] | map(parse_age) | map(describe)
["age 42", "cannot read -3: negative", "cannot read forty: not a number"]

The ways out

The ways out of a Result mirror Option's. map transforms the value an ok holds and leaves an err as it is. and_then chains a second computation that may fail. unwrap_or leaves the result with a value for the error case:

fn parse(text: String) : Result<Int, String> = match text.to_int() {
    some(n) => ok(n),
    none => err(fmt("not a number: {}", text)),
}

[
    parse("7").map(n => n * 2).unwrap_or(0),
    parse("x").map(n => n * 2).unwrap_or(0),
    parse("7").and_then(n => parse("1").map(m => n + m)).unwrap_or(0)
]
[14, 0, 8]

map_err transforms the error and leaves an ok as it is. ok() and err() give either side as an Option, the other side being none:

fn parse(text: String) : Result<Int, String> = match text.to_int() {
    some(n) => ok(n),
    none => err("no"),
}
[parse("4").ok(), parse("x").ok(), parse("x").err()]
[4, none, "no"]

Over a collection of results, oks() keeps the values of the successes and errs() the errors of the failures, each at its own type:

fn parse(text: String) : Result<Int, String> = match text.to_int() {
    some(n) => ok(n),
    none => err(fmt("not a number: {}", text)),
}
rs = ["4", "x", "9"] | map(parse)
[rs | filter(r => r.is_ok()) | size, rs | oks(), rs | errs()]
[2, [4, 9], ["not a number: x"]]

There is no unwrap

Every way out of a Result says what the error case means: a match names it, unwrap_or replaces it, ok() makes it an absence. There is no operation that takes the value and stops the program otherwise. parse("4").unwrap() is a name error, since no such function exists. A failure the program cannot handle as a value is an evaluation failure, raised by the runtime and not caught by HQL code; the two are kept apart, and a Result is for the failures a program expects of its input.

Where results come from

Parsing, decoding and validation are the operations whose failure is an expected consequence of their input, and each has a Result form. validate(T) reads a tree as a contract and gives Result<T, Invalid>, with every reason for a failure inside the error; over a collection it is elementwise, and oks(), errs() and fork route the two sides. Validation as a value is that pattern in full.