Command Line Interpreters in Elm, Part I

James Carlson
5 min readApr 11, 2020
Elm RPN calculator on the command-line

The Elm programming language is best known for building web apps. Turns out, however, that one can also build something quite different: command-line programs like the one you see on the left. It is a simple stack-based calculator. The first command computes 2 + 3 and leaves 5 on the stack. The next command adds 1 to whatever is on top of the stack. Each command takes its arguments, either from the command line or the stack, and leaves the result on top of the stack. There are some useful variants like \sub x y = sub y x that does subtraction in the opposite of the usual order. Thus, if 5 is on top of the stack, and you say \sub 1, then 4 will be on top. And if you subsequently say \div 4, then 1.333… will be on top. In addition, there are more exotic commands, such as push 1 2 3 4, which push a sequence of numbers onto the stack. Then the command sum will add them up, leaving 10 on top. The sister command av will compute their average, leaving 2.5 on top.

This is article is part one of a two-part series. In the first, we will learn how to build a one-shot command-line program, then a very simple command-line interpreter. In part two, I’ll describe an architecture for building more sophisticated command-line interpreters like the one you see in action above. I will close part II with a discussion a tool built using these ideas to help with another project (parsing block-structured markup languages, transforming abstract syntax trees)

All the code is on GitHub. I will put annotated references at the end

A one-shot command-line program

By a one-shot command-line program, I mean a program that takes its arguments on the command line, does its work, then exits. You have used many of these: ls, cat, grep, etc. Here is our example in action:

$ node src/cli.js 33Input:  33
Output: 100

The program took the argument 33, did some computation, and displayed the result. It was obtained by the following rule: if the input is even, divide it by 2; if it is odd, multiply it by 3 and add 1. Not an earth-shaking computation, but the numbers you get by repeatedly applying this rule have some interesting properties. In any case, the rule is an instance of a general pattern: transform input to output.

The program has two parts, a small Javascript program, cli.js, and an Elm program, Main.elm, which can range in size from tiny to huge. Here is the wiring diagram:

Wiring diagram for headless Elm apps

The Javascript Program

The Javascript program cli.js is the middleman between you, typing away at the terminal, and the Elm program, which is a black box receiving inputs from cli.js, doing some computation, and sending them back. The communication between the two programs is implemented using ports. Here is the code, just 9 lines, excluding blanks and comments.

// File: cli.js// Link to compiled Elm code main.js
var Elm = require('./main').Elm;
var main = Elm.Main.init();

// Get data from the command line
var args = process.argv.slice(2);
var input = parseInt(args[0])
console.log("\n Input: ", input)

// Send data to the worker
main.ports.get.send(input);

// Get data from the worker
main.ports.put.subscribe(function(data) {
console.log(" Output: " + JSON.stringify(data) + "\n");
});

The JS program does no error checking. It grabs the command-line argument converts it to an integer, and sends it to the Elm program via main.ports.get.send.input. Here get is the name of the Elm function which gets data from JS. The JS program also listens for data from the Elm program. using main.ports.put.subscribe. Here put is the name of the Elm function which sends data to JS.

The Elm Program

As noted above, the Elm program has two ports, get and put . The get port listens for input from cli.js. When it receives data, say the integer 33, it sends the message Input 33. That message is processed by the following line in the update function:

case msg of
Input input -> ( model, put (transform input))

That is, some computation is carried out by transform, and the result is sent to JS by put. We have used the type aliases InputType and OutputType to emphasize the generic nature of the input-transform-output schema. The data types can be anything, as can be transform.

Below is the code, 34 lines, of which 30 define the “black-box” framework, with 4 more for the transform function.

port module Main exposing (main)

import Platform exposing (Program)

type alias InputType = Int
type alias OutputType = Int

port get : (InputType -> msg) -> Sub msg
port put : OutputType -> Cmd msg

main : Program Flags Model Msg
main =
Platform.worker
{ init = init
, update = update
, subscriptions = subscriptions
}

type alias Model = ()
type Msg = Input Int
type alias Flags = ()


init : Flags -> ( Model, Cmd Msg )
init _ = ( (), Cmd.none )

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Input input -> ( model, put (transform input))

subscriptions : Model -> Sub Msg
subscriptions _ =
get Input

{- Below is the input-to-output transformation.
It could be anything. Here we have something
simple for demonstration purposes.
-}

transform : InputType -> OutputType
transform k =
case modBy 2 k == 0 of
True -> k // 2
False -> 3*k+ 1

A REPL (Read Eval Print Loop)

We’ll now look a slightly more sophisticated JS program that provides a repl that talks to the same Elm program. Here is a sample session:

$ node src/repl.js
> 33
100
> 100
50
> 50
25
> 25
76

In the second version, you can keep issuing commands. To quit, type ctrl-D. There is only one acceptable command, an integer, and there is no error-handling. If you type “test”, the program will crash. In Part II we will discuss a more sophisticated example that implements many commands and which handles errors gracefully. Here is the code:

// File: src/repl.jsconst repl = require('repl');

// Link to Elm code
var Elm = require('./main').Elm;
var main = Elm.Main.init();

// Eval function for the repl
function eval(input, _, __, callback) {
main.ports.put.subscribe(
function putCallback (data) {
main.ports.put.unsubscribe(putCallback)
callback(null, data)
}
)
main.ports.get.send(Number(input))
}

repl.start({ prompt: '> ', eval: eval });

References

--

--