ral — a typed, capability-secure shell

3 min read Original article ↗

Running a command is an algebraic effect.

A typed shell that separates values and commands.

value

A value is.

Inert data — a list, a record, JSON. You read it; you never run it.

command

A command does.

An effect — output to catch, failure to handle, authority to confine.

the two sigils

A value is a noun, a command a verb, and ral writes the difference into the notation itself: $ retrieves a value, ! runs a stored command. One asks for data, the other does something — and the shell's oldest ambiguity, between inert text and a live instruction, has nowhere left to live.

the same job, twice

In ral, a command's output is a value you hold, and a command's failure is a value too. Fan the work across cores, catch each result into a record, and the URLs that failed come back in hand instead of vanishing down a pipe.

bash

# which URLs failed? you don't find out.
cat urls.txt | xargs -P8 -I{} \
  curl -fsSL -o "out/$(basename {})" {}

ral

let urls = from-lines-list 'urls.txt'
let report = par { |u|
    try {
        let data = curl -fsSL $u | from-json
        return [url: $u, result: `ok $data]
    } { |e| return [url: $u, result: `err $e[message]] }
} $urls 8
to-json $report > 'manifest.json'

what falls out

Once they are apart, the data half becomes something a shell has never been: a small, purely functional language. Variables don't mutate, so a name means one thing throughout. There are no subshells, no $(...) — you build data up and take it apart with map, filter, and fold, the way you would in any functional language.

Because nothing mutates, running work in parallel loses its usual hazards: fan a job across a pool of workers and there are no locks and no races. The results come back in the order you sent them. A command's output is captured as a String — a value, not a text stream you re-split and re-quote by hand. When you want a list, a record, or JSON, you cross to it on purpose with a codec like from-json, and the type is fixed at that one boundary.

A command is a system call: catch its output into a typed variable, catch its failure, substitute your own interpretation, or confine it in an OS sandbox. Audit the whole tree of commands a script produced: its arguments, its output, and the permission decisions that framed it.

get ral

curl -fsSL https://lambdabetaeta.github.io/ral/scripts/install.sh | sh

foundations

None of this is improvised. Separating values from commands is call-by-push-value (Levy); treating a command as an operation whose interpretation is supplied separately is algebraic effects (Plotkin and Power). The whole language is written down — a specification and a rationale, not a pile of features. And it is not POSIX, by design: your bash scripts won't run.