Results

Partly built

result<'e, 'a> from the standard library, hash-identical to Unison's Either, and Bytes.fromBase16.

stdlib/std.clay declares an ordinary generic variant for operations that can fail with a reason:

type result<'e, 'a> = | Err('e) | Ok('a)

let decode = (hex: bytes): result<string, bytes> => Bytes.fromBase16(hex)

let sizeOf = (hex: bytes): int => switch decode(hex) {
| Ok(b) => Bytes.size(b)
| Err(_) => -1
}
Verified through UCM. The error parameter comes first in the header.

It is declared so that it hashes identically to Unison’s Either: Err is Left and Ok is Right, and the header lists the error parameter first. The order matters. A header with the success parameter first has the same declaration hash, but swaps the roles of the two constructors, so terms built from it hash differently. That is what lets a Unison builtin that returns Either Text Bytes be typed as result<string, bytes> with no conversion.

Bytes.fromBase16(b) takes ASCII hex digits (as bytes, the output of Bytes.toBase16) and gives Ok(bytes), or Err(message) for an odd number of digits (base16: input: invalid length, checked first) or a non-hex digit (base16: input: invalid encoding at offset: N, N the start of the bad two-digit pair). Upper and lower case are both accepted. The messages are UCM’s own.

The standard library also has resultIsOk, resultWithDefault and resultMap, for result<string, int> only: Clay functions cannot take type parameters yet. To use the type, clay add the standard library (or declare result as above in your own file). For errors with a name, the standard library has failure, which is Unison’s own io2.Failure (same hash): @unison("io2.Failure") type failure = {kind: typelink, message: string, payload: any}. kind is a type link naming the kind of failure (typeLink(decodeError)), message is for people, payload is an any (Any.make(x); read it back with Any.unsafeExtract, which is unsound at the wrong type). Helpers are failureNew, failureText, failurePayload, failureIsKind, and, for result<failure, int>, resultFlatMap, resultOrElse and resultToOption; resultFromText(r, kind) lifts a text-error result such as Bytes.fromBase16’s. The carrier result is hash-equal to Either, so a result<failure, a> is hash-equal to Unison’s Either Failure a. An expected outside failure is a value in the type; a bug is a defect that aborts the run. Not built: retry and timeout, error unions and tagged errors.

Raising instead of returning

When a caller would rather not thread a result through every step, the standard library has an Exception ability whose one operation, raise, takes a failure and never returns (it is hash-identical to Unison base’s own Exception). resultExpect turns an Err into a raise; exceptionToResult is the handler that turns a raise back into a value, so the two conventions meet at an ordinary handle:

ability Exception {
raise: <'x> failure => 'x
}

let resultExpect<Exception> = (r: result<failure, int>): int =>
switch r {
  | Ok(x) => x
  | Err(f) => Exception.raise(f)
}
From stdlib/std.clay, which also declares result and failure. Shown for the shape; it only compiles together with those.

resultBug is the third answer for a case that cannot happen: it aborts the run with the failure’s message. A raise is an effect, so a function that can raise says so in its <Exception> list, and the checker refuses a call that forgets to handle or declare it.