Barebones Architecture in Elm

Motivation and Background

In the process of writing larger applications in Elm I find it far too easy to initially focus on one small part of the problem while losing perspective on the bigger picture. For instance, I will begin by rendering a simple scene, followed by splitting out components, and finally adding in signals. While this is generally a good approach, in my mind, it is sometimes difficult to retrofit patterns onto the basis of the application I have already written.

There are some really excellent resources covering the architecture of applications in the Elm language. The canonical article on the subject can be found on the official Elm site and covers everything at a high level. The best example is likely TodoMVC ( source ) ported to Elm which uses many the concepts from the article to create a rich and simple interface, showcasing just how powerful Elm is. Another great and extremely polished example is Dreamwriter ( source ), which also incorporates the separation of components as outlined in the original article.

The only problem with these examples, and the theory behind them, is that they are too fleshed out, too complete. It is difficult to extract the core concepts from the specific details of the problem they are solving. In attempting to retrofit this pattern onto my own programs, I had to remove everything extraneous and reduce the implementation to the fundamentals.

A Simple Example

First, I will present the source and result of a very, very basic (practically trivial) user interface in Elm. It consists of nothing more that a button that increments a value, but this simple scene gives us the foundation upon which we can build larger applications.

barebones_architecture.elm

Source
import Signal

import Text

import Graphics.Input as Input

import Graphics.Element (Element)
import Graphics.Element as Element

type alias State = { value: Int }

type Action
  = NoOp
  | Increment

main : Signal Element
main = Signal.map scene state

scene : State -> Element
scene state =
  Element.flow Element.down [incrementButton, (Text.asText state.value)]

state : Signal State
state = Signal.foldp step initialState (Signal.subscribe actions)

step : Action -> State -> State
step action state =
  case action of
    NoOp -> state
    Increment -> { state | value <- state.value + 1 }

initialState : State
initialState = { value = 0 }

actions : Signal.Channel Action
actions = Signal.channel NoOp

incrementButton : Element
incrementButton =
  Input.button (Signal.send actions Increment) "Increment"
View Result

The Moving Parts

This example consists of the four core components for any such application:

  1. User input represented as a union type
  2. A signal channel for passing messages generated by user input
  3. State dependence on a subscription to that channel
  4. Rendering a scene based on the current state

We will discuss each in turn.

User Input Union Type

Most applications are not very interesting without user input. We choose to represent all user input as a union type This gives us the ability to easily pattern match on the user action in a case statement, which will be useful very soon. In this particular example, the Action type can only consist of either a NoOp or Increment.

Signal Channel for Message Passing

We then create a signal channel of actions. The actions method creates a new channel that will default to the NoOp value. The incrementButton function creates a new button that will send the Increment value to the channel.

Subscribing and Updating State

The state function begins to really pull all the pieces together. Here, we use Signal.foldp to create a past-dependent Signal State. The State type alias is defined as having only one field, a value of type Int. We start with an initialState where the value field is set to zero. The signal we provide to the foldp function is the result of passing the actions channel to the Signal.subscribe function.

We then define a step function, of type Action -> State -> State. Because of the pattern matching on union types, the case statement is very simple to follow: if the action parameter is a NoOp, simply return the original state, but if it is an Increment action, return a new State record with its value increased by one.

Rendering a State Dependent Scene

The final piece of glue is very typical in Elm, the main function of type Signal Element. In this very simple example, we are using Signal.map to call scene with the current state.

Extensibility

While this example does not actually do particularly much, that is precisely the point. You can transparently observe how the pieces fit together to make up the core of the application. In practice, there are many more steps to be made, but the general principles remain the same:

  1. A central location for state in the application
  2. Accepting user input to mutate that state
  3. Outputting some representation of that state

Generally speaking, to add real functionality, we only need to add more values to the Action union type, extend the case statement in step, and create more sources of input like incrementButton. In reality, however, our applications will be much more maintainable if we create separate components and nested Action and State types (which the architecture article covers in detail).

In conclusion, the basic pattern that is being used in more complex and feature rich Elm applications can be distilled to very little, but is an extremely powerful concept that can be extended to a great degree. This is the case to such a degree that I will likely start with a core similar to this, rather than some trivial scene, when writing new applications.