Bindings & functions
Implementedlet, functions, blocks and if/else — the working core of the language today.
Everything in Clay is an expression, and the last expression in a block is its
value — an explicit return for early exit is available too and is covered at the
end of this page.
let
let binds a name to a value. A top-level let may carry parameter and return
type annotations; when present they’re checked against the value, and an annotated
and an unannotated definition of the same thing are literally the same object
(same hash — see Hashing & storage).
let add = (a: int, b: int): int => a + b
let greet = (name: string): string => {
let greeting = "Hello, "
greeting ++ name
}Inside a block, a let sees only the bindings written before it — unlike Unison’s
own block bindings, which are recursive and see the whole block. Clay’s emitter
renames every local binder under the hood to keep that difference from leaking into
the hash.
Functions
Functions are arrow expressions: (params) => body. Parameter and return type
annotations are optional at the top level — a definition with no annotation at all
is inferred (see Types & inference).
Functions as values
A function does not have to be written out. A name can be bound to another function, to a builtin, or to a
constructor, and _ in an argument list leaves a hole, giving a function of what is missing (partial application):
let addTo = (a: int, b: int): int => a + b
let inc = addTo(1, _)
let negate = Int.negate
let wrap = Some
let twice = (f, x) => f(f(x))
let three = twice(inc, 1)inc has type int -> int, and callers cannot tell it from a function written with a body. Some is a real
constructor term, not a lambda, so wrap hashes the way Unison’s does. A stored value may be polymorphic too
(let empty = [] has type list<a>, and each use picks its own element type). Fewer arguments than parameters,
with the hole spelled out, is partial application; too many is still a type error.
Delayed computations
thunk(e) delays e until someone calls the result, and force(t) runs it; the type is written thunk<T> and is the
same as () => T. It is Unison’s do, and it behaves like it: the parameter it ignores is not forced to be unit, so a
thunk can be handed to a function that expects a one-argument function. On its own it still has type () => T.
let apply = (f, x) => f(x)
let delayed = thunk(apply(thunk(7), "ignored"))
let run = (job: thunk<int>): int => force(job)
let answer = run(delayed)One Unison detail carries over: a thunk bound with let is generalized there, and its parameter falls back to unit
at that point. So let t = thunk(1) is a () => int for good, while apply(thunk(7), "ignored"), where the thunk is an
argument, can take anything.
Characters
A char is one Unicode code point, written between single quotes ('a', '\n', '\u{1F600}'). Chars compare and
sort by code point, and a string is still a separate type.
let isVowel = (c: char): bool =>
c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'if / else
if/else is an expression, not a statement, and both branches must produce the
same type. else is required (there’s no such thing as a T | unit from an if
with no else).
let abs = (n: int): int =>
if n < 0 { 0 - n } else { n }Early return, and options
An explicit return exits the nearest function early (it also works inside a loop,
next to break and continue). It desugars before hashing, so a guard hashes like
the equivalent if/else. A function may return an option<T> (Unison’s builtin
Optional); the constructors are written Some(x) and None():
let safeDiv = (a: int, b: int): option<int> => {
if b == 0 { return None() }
Some(a / b)
}Take an option apart with switch, or give it a default with ??.
Optional chaining
a?.field reads a field through an option: None stays None, Some(r) becomes Some(r.field). It desugars to a
switch before hashing and does not flatten, so pair it with ?? for a default.
type server = {port: int, host: string}
type config = {server: option<server>}
let portOf = (c: config): int => c.server?.port ?? 8080The record type on the left must be statically known (an option-typed field of a typed record, or a chain of ?.).