PFCL  ·  Pure Functional Composition Language

Math lands on the machine.


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
  }

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?