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.