A small pure functional language. Functions are mathematics; effects are plain data a host runs; identity is the hash of the body. How that math reaches the machine, in three principles and what they buy together.
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.
What the three buy together
A program is a set of composable pure, total functions. None of them performs anything — the only way out is a command handed back — so a program's reach is the set of command kinds it can produce. Not behaviour you take on trust; a set you can look at.
The result is a program whose dependencies, identity, capability surface, and termination obligations can be derived from the program itself before it runs.
On the far side sits a switch over those command kinds — the only place anything happens. It grows when a new command kind appears, not when a new program does.
Which is why so much of the usual programming apparatus has nothing to do here. No package manager, because there are no versions to resolve. No mocks inside the computation, because a test reads the commands your function already returns. No effectful step-through debugging, because a value has no execution history — it's a function of its inputs, the same on every run.
What's left is the one question no language can answer for you: is this the functionality I meant?