Making Functional Machines with Elm

James Carlson
8 min readJun 1, 2018

I recently encountered a little text processing problem in an app I was working on (see LaTeX in the Browser). Here is a version of the problem. Suppose given a list of a list of strings, say

["He", "said", "," , "Hi", "there", "!"] 

The task is to join the strings to form good prose. The following is not the way to do it:

> String.join " " ["He", "said", "," , "Hi", "there", "!"]
"He said , Hi there !"

What is needed is a rule that looks at a string and and the string following it, and then joins them either with a space or no space, depending on some rule. We may want to produce the “join” directly, e.g.,

"He said, Hi there!"

Or we may wish to first annotate the list of strings for further processing, like this:

[("He", S), ("said", N), (",", S) , ("Hi", S), 
("there", N), ("!", P)]

The symbol S means “join to the following string with a space, N means join with no space, and P means the end of a paragraph — so add two blank lines if there is text following.

A More General Problem

This little problem leads to a more general one. Suppose that we have a list of things a_1, a_2, a_3, ... that we want to turn into a list b_1, b2, b3, ... The rule for computing b_k is a function of a_(k-1), a_k , and a_(k+1) — the current element, the element before, and the element after. Let’s try to solve the general problem, and then apply the solution to some special cases.

We can imagine our computation as being carried out by a machine that accepts an input tape, reads from it, and, consulting its internal state at each step, adds an element to an output tape. As a simple test case, our input tape will be a list of integers, as will be the output tape. See if you can guess the rule:

Step 0:  Input = [0, 1, 2, 3, 4]  Output = [ ]
Step 1: Input = [1, 2, 3, 4] Output = [1]
Step 2: Input = [2, 3, 4] Output = [1, 3]
Step 3: Input = [3, 4] Output = [1, 3, 6]
Step 4: Input = [4] Output = [1, 3, 6, 9]
Step 5: Input = [ ] Output = [1, 3, 6, 9, 7]

Each element of the output list is the sum of the corresponding element of the input list and its left and right neighbors. Look at the number 6 in the output list. It has position 2 (counting from 0, as computer scientists do). The number in position 2 in the input list is 2; the sum of this number and its two neighbors is 6.

What we want is a general solution to the general problem. That way we can test it on special cases like the one just described, then go on to confidently apply it in harder cases. All we have to do is verify the rule for computing output values.

This is what typed functional languages like Elm and Haskell allow us to do — capture a “pattern of computation.”

A List Processing Machine: Designing the Main Types

We will design a ListMachine module which exports just two things: a function called run and a type, InternalState. The first implements a way of transforming a list of a's into a list of b's. The second defines the internal state of the machine, and has type signature

type alias InternalState a = 
{ before: Maybe a,
current: Maybe a,
after: Maybe a,
inputList: List a }

The InternalState is what is needed to capture the idea of a “tape” of values where there is a value that is “currently being read” and where the machine has access to the values immediately to the left and right of the current on. We call these values “before” (left), “current”, and “after” (right). The Maybe values are needed because, for example, internalState.before may not be defined before the first cell of the tape is read.

To define an actual machine, one needs a function that computes a value of type b in terms of the internal state. Such a function has type

InternalState a -> b

The function that “runs” the tape through the machine has type

run : (InternalState a -> b) -> List a -> List b

Here is the output function for the example above:

sumState : InternalState Int -> Int 
sumState internalState =
let
a = internalState.before |> Maybe.withDefault 0
b = internalState.current |> Maybe.withDefault 0
c = internalState.after |> Maybe.withDefault 0
in
a + b + c

If all goes well, we can do this:

ListMachine.run sumState [1,2,3,4]
==> [1, 3, 6, 9, 7]

Building the Machine

To build the machine, we need to capture not only the InternalState, with its input tape and three “registers,” before, current, and after, but also the current value of the output tape, whose elements are constructed one-by-one as the elements of the input tape are “read.” Here is the type of the full machine state:

MachineState a b = 
{internalState: InternalState a, outputList: List b}

The machine will process its tape using a foldl function, transforming an initial MachineState into a final one, where the final one carries the computed result in its outputList field. The type signature of foldl is

List.foldl : (u -> v -> v) -> v -> List u -> v

Let’s now compare types, assuming that the value produced by foldl is Machine a b. Then we must set v = Machine a b, and we must set u = a, since the input tape has type List a. The foldl we use in this context must therefore have the signature

(a -> (Machine a b) -> (Machine a b)) -> (Machine a b) -> List a -> Machine a b

This type alias is a bit long, so let’s agree on the following definitions. First, a generic Reducer (something consumed by foldl) has type alias u -> v -> v. Second, a ListMachine.Reducer is

type alias Reducer a b = a -> MachineState a b -> MachineState a b

Then our foldl type alias can be written as

ApplyReducerToMachine : Reducer a b -> (Machine a b) -> List a -> Machine a b

This looks right: a reducer is used with an initial machine state and a list of inputs of type a to produce a final machine state. That final state holds an output list of elements of type b. From the type analysis, we deduce the function definition. In fact, given the types and given the fact that we use foldl, the only possible definition is the following one:

applyReducerToMachineState : Reducer a b -> MachineState a b 
-> List a -> MachineState a b applyReducerToMachineState reducer initialState inputList =
List.foldl reducer initialState inputList

Computing the initial state

Suppose that we have a function initialMachineState inputList that builds a MachineState from an inputList. Then we can define a function applyReducer that takes a reducer and a list of a's and transforms it into a list of b's:

applyReducer : Reducer a b -> List a -> List b
applyReducer reducer inputList =
let
initialState = initialMachineState inputList
finalState = applyReducerToMachineState reducer initialState
inputList
in
List.reverse finalState.outputList

We compute the initial state, run the reducer, then extract the output list from the final state. To set up the initial state, we first set up the initial internal state

makeInitialState : List a -> InternalState a
makeInitialState inputList =
{before = Nothing
, current = List.head inputList
, after = List.head (List.drop 1 inputList)
, inputList = inputList
}

To set up the initial machine state, define an empty output list:

initialMachineState inputList = 
{internalState = makeInitialState inputList, outputList = []}

Computing the next internal state

To compute the next internal state, we use the function definition below. Note that the next internal state is a function of only one argument: the previous internal state. Put in other terms, this operation involves only reading from the input tape.

nextInternalState : InternalState a -> InternalState a
nextInternalState internalState_ =
let
nextInputList_ = List.drop 1 internalState_.inputList
in
{
before = internalState_.current
, current = internalState_.after
, after = List.head (List.drop 1 nextInputList_)
, inputList = nextInputList_
}

Reducing the input

We are almost finished. The penultimate step is to design a function for making reducers of type a b from functions of type InternalState a -> b. This is accomplished by the function makeReducer. It takes a single argument, an output function f : InternalState a -> b.

makeReducer : (InternalState a -> b) -> Reducer a b
makeReducer computeOutput input machineState =
let
nextInputList = List.drop 1 machineState.state.inputList
nextInternalState_ = nextInternalState machineState.state
newOutput = computeOutput machineState.state
outputList = newOutput::machineState.outputList
in
{internalState = nextInternalState_, outputList = outputList}

Once we have makeReducer, we can define ListMachine.run:

run : (InternalState a -> b) -> List a -> List b
run outputFunction inputList =
applyReducer (makeReducer outputFunction) inputList

At long last we have a simple API. To define a ListMachine, we need only define an outputFunction of type InternalState a -> b. To run the resulting machine, execute

run theOutputFunction theInputList

Example 1: the sumState machine

Recall the output function for lists of numbers:

sumState : InternalState Int -> Int 
sumState internalState =
let
a = internalState.before |> Maybe.withDefault 0
b = internalState.current |> Maybe.withDefault 0
c = internalState.after |> Maybe.withDefault 0
in
a + b + c

It sums up the values of the the before, current, and after fields of the internalState. Here the computation we did at the beginning of this article

ListMachine.run sumState [0, 1, 2, 3, 4]
=> [1, 3, 6, 9, 7].

Example 2: String processing

Let’s go back to the string list example. Define a new output function:

type SpacingSymbol = Space | NoSpace | EndParagraphstringJoiner : InternalState String -> (String, SpacingSymbol)
stringJoiner internalState =
let
b = internalState.current |> Maybe.withDefault ""
c = internalState.after |> Maybe.withDefault ""
symbol = if internalState.after == Nothing then
EndParagraph
else if List.member (String.left 1 c)
[",", ";", ".", ":", "?", "!"] then
NoSpace
else
Space
in
(b, symbol)

We can run it to get an annotated list:

ListMachine.run stringJoiner ["He", "said", ",", "Wow", "!"]
==> [("He",Space),("said",NoSpace),(",",Space),("Wow",NoSpace),("!",EndParagraph)]

Example 3: a variant of String processing

Here is a slightly different output function:

stringJoiner2 : InternalState String -> String
stringJoiner2 internalState =
let
b = internalState.current |> Maybe.withDefault ""
c = internalState.after |> Maybe.withDefault ""
output = if internalState.after == Nothing then
b ++ "\n\n"
else if List.member (String.left 1 c)
[",", ";", ".", ":", "?", "!"] then
b
else
b ++ " "
in
output

And here is the result:

ListMachine.run stringJoiner2b ["He", "said", ",", "Wow", "!"]
==> ["He ","said",", ","Wow","!\n\n"]

Conclusion

Elm’s type system is a powerful mechanism for abstraction. Once the underlying pattern of computation for a problem is extracted and given form with suitable type signatures and functions, it is often possible to give quite general solution with a simple API that hides a fair amount of complexity. The ListMachine package exemplifies this philosophy.

Reference: GitHub Repo for ListMachines or this Ellie.

--

--