6. Abilities, records, variants

Three features, one hashing scheme: how each is lowered so the kernel needed almost nothing new.

Each of these was added as a rung: grammar, AST/Core, emitter, checker, storage, editor. The recurring lesson is that the hashing scheme is smaller than the surface. Unison represents all three with the same machinery, so Clay reuses it.

Variants

type shape = | Circle(int) | Rect(int, int)
let area = (n: int): int => {
let c = Circle(n)
switch c {
  | Circle(r) => r
  | Rect(w, h) => w
}
}
let a = area(3)

Emitted Unison (always structural, never nominal):

structural type Rec_shape = Circle builtins.Int | Rect builtins.Int builtins.Int

area : builtins.Int -> builtins.Int
area n =
  c_1 = Circle n
  (match c_1 with
    Circle r -> r
    Rect w h -> w)

A sum type is the same DataDeclaration shape a record uses, with N constructors instead of 1. Constructors are referenced by name in Core and by index only when the kernel term is built. The order of switch cases is hashed as written, so it is load-bearing.

A real bug this exposed: constructors are ranked by a sort key; it used the wrong type hasher, invisible until two constructors with several arguments were compared. Fixed in both kernels (docs/hashing-notes.md, “Variants”). Lesson: a fixture set is only as strong as its shapes.

Records

A record type is a single-constructor data declaration. Field access and update-spread need no new Core tag: the compiler derives an accessor and a setter as ordinary definitions, and .field lowers to a call of the accessor. Fields are reordered to declared order during lowering, so {b: 1, a: 2} and {a: 2, b: 1} are one term.

Abilities (effects)

ability Console { print: string => unit }
let hello<Console> = (name: string): unit => Console.print("Hi " ++ name)
let greet: string =
handle hello("World") {
  done(x) => "done";
  Console.print(msg, resume) => msg
}
structural ability Console where
  print : builtins.Text ->{Console} ()

hello : builtins.Text ->{Console} ()
hello name = Console.print ((builtins.Text.++) "Hi " name)

greet : builtins.Text
greet =
  (handle (hello "World") with cases
    {x} -> "done"
    {Console.print msg -> resume} -> msg)

The declared <Console> list is checked against what the body actually performs. done(x) handles the normal-return case; Console.print(msg, resume) handles the operation with a continuation resume. In hashing, an ability is a declaration hashed like a type, with each operation identified by index.

Generics (any number of type parameters), exhaustiveness checking, nested patterns and ability polymorphism (<'e>) are all built since this chapter was first written. Same-shape types (two records or variants with identical shapes are one type to Unison) keep their own names per label, and unique type / unique["guid"] type mints a nominal type with a GUID in its hash. Still open: constructor, operation and field rename. See docs/open-issues.md.

Next: Interpreting.