Variants & switch
ImplementedSum types, constructors, exhaustive switch, generics and recursive types.
A variant is a named sum type. Like records and abilities it is structural: its identity is a function of its
constructors’ shapes, not a generated id. switch takes one apart and the checker insists every case is covered.
type shape = | Circle(int) | Rect(int, int)
let area = (s: shape): int => switch s {
| Circle(r) => r * r * 3
| Rect(w, h) => w * h
}
let total = area(Rect(2, 5))- Constructors are written with parentheses when they are built, even when nullary (
None()); a nullary constructor in a pattern is just its name (| Empty =>). - Type parameters work (one to N), as do self-recursive types (
type tree = | Leaf | Node(tree, int, tree)) and mutually recursive ones. - A record, variant or generic can be named in a parameter, return or
letannotation.
Taking values apart
A switch arm is | pattern => body. A pattern can be a constructor, a literal, a variable, a list or tuple pattern,
or _ (an ordinary identifier the compiler treats as a wildcard), and they nest. Two additions make arms more precise
without nesting more switches: a guard (if condition after the pattern) and an as-pattern (pattern as name,
which also binds the whole matched value). The example below uses both, and also passes a constructor as a function:
type shape = | Circle(int) | Rect(int, int) | Empty
let area = (s: shape): int => switch s {
| Circle(r) => r * r * 3
| Rect(w, h) => w * h
| Empty => 0
}
let describe = (s: shape): string => switch s {
| Circle(r) if r > 100 => "huge circle"
| Circle(_) => "circle"
| Rect(w, h) as whole if w == h => "square of area " ++ Int.toText(area(whole))
| Rect(_, _) => "rectangle"
| Empty => "nothing"
}
let circles = List.map([1, 2, 3], Circle)Read describe from the top. The first arm only matches a circle whose radius passes the guard; when the guard is false
the next arm is tried, so a plain Circle(_) below it catches the rest. The Rect arm does two things at once:
Rect(w, h) names the parts for the guard to compare, and as whole keeps the entire Rect so area can be called
on it without rebuilding it. A guard is part of the arm’s identity (Unison hashes it between the pattern and the body), and a guarded arm does not
count toward exhaustiveness: the checker still wants an unguarded arm for every case.
The last line shows that a bare constructor name is a function value: Circle has type int -> shape and can be handed to
List.map. It is a real constructor term rather than a lambda, so it hashes differently from (r) => Circle(r), just as
it does in Unison. A nullary constructor must still be written None() when used as a value.
Literal patterns
Literal patterns work on int, nat, string, bool and bytes. A negative int literal is a single pattern and
a nat takes the n suffix:
let compareWord = (a: string, b: string): string => switch Universal.compare(a, b) {
| -1 => "before"
| 0 => "same"
| _ => "after"
}
let countName = (n: nat): string => switch n {
| 0n => "none"
| 1n => "one"
| _ => "many"
}-1 is parsed as one literal (not as a subtraction), and a pattern below the smallest int is refused.
Several types of the same shape
Types are structural: two types with the same shape hash to one declaration, whatever they are called. Clay keeps the
names you gave each one, so both print back the way you wrote them, and a term built with either has one hash. When you
want two types that must never be confused, make them nominal with unique:
unique type userId = {value: nat}
unique type orderId = {value: nat}
type point = {x: int, y: int}
type vec = {x: int, y: int}
let here: point = {x: 0, y: 0}
let heading: vec = {x: 1, y: 0}
let sameX = here.x == heading.xunique type mints a fresh identifier the first time the type is added and then keeps it; write unique["guid"] type ...
to fix the identifier yourself (this is what you want when a type must hash the same on two machines). Without unique,
a record literal whose type cannot be settled from its fields alone needs an annotation, as here and heading have
above, because both point and vec fit.
Renaming a constructor is not supported yet (renaming the type’s own name is).