A small pure functional language. Functions are mathematics; effects are plain data a host runs; identity is the hash of the body. How the math gets to the machine, in three principles.
A function gets a body — its identity is the hash of that body
name: math.mean
type: List<Float> -> Maybe<Float>
body: |
\(xs).
match list.is_empty(xs) with
| true -> nothing
| false ->
just(float.divide(
list.fold_left(\(acc, x). float.add(acc, x), 0.0, xs),
int.to_float(list.length(xs))))
$ sha256sum <typed body> a3f9b2c1d4e5…f0a1
A function describes effects as data — a host performs them
name: app.hello
type: List<String> -> List<Command>
emits: [stdout] # the command kinds it emits
body: |
\(args).
[ {kind: "stdout", text: "hello, machine"} ]
the function performs nothing — it returns a list of records:
[ {kind: "stdout", text: "hello, machine"} ]
a host reads them — a switch, in whatever language runs it:
// executor — any language; a switch over command kinds
for (const cmd of commands)
switch (cmd.kind) {
case "stdout": write(cmd.text); break;
// a host implements only the kinds its target offers
}
- Decoupled. The function returns descriptions and performs nothing. Purity holds all the way to the host boundary.
- A protocol, not an API. A command is a record you return, not a call into a runtime — so an effect is a value: inspectable, loggable, replayable, not an action already taken. The contract is the record schema, so anything that can read records is a valid host. Any executor, in any language; the same program runs anywhere one exists.
- Bound to the host's capabilities. Effects available are exactly the command kinds the host implements. A kind the host does not handle is inert. The vocabulary is open — a host extends what it handles by extending its switch.