Elm for the Frontend, Right Now

NOTE: This article was written for Elm 0.16 and is now out of date. Many of the concepts discussed in this article were removed in the upgrade to 0.17, and some of the Elm syntax was changed in the move to 0.18. Please read the article we rewrote for Elm 0.18 in November 2016.


The Ruby Rogues recently did a podcast (#212) about the Elm programming language. Go feast your ears on it right now; this post builds upon the episode. I also want you to listen to it because the episode will do a better job than I can of convincing you about the benefits of: static typing, functional programming, and purity. And it will do a better job precisely because it ignores all those things and just talks about how to build cool stuff faster and with fewer errors.

Elm is an exciting language. It aims its scissors squarely at the knot of complexity that is frontend development. The tangle of threads, HTML, CSS, and JavaScript, make web development awkward. JavaScript is especially troublesome, it is designed to swallow errors and move on – something that we know is a wrong idea in programming!

Elm actually allows for a no-kidding MVC architecture that I haven't really seen elsewhere. Purists may object that that's "not really MVC," but I would counter that it is closer to the intent of MVC than most implementations. The reason it pulls this off is simply: purity.

Application updates happen by starting with a model of the application state, receiving an incoming action, and then creating a new model. The view is at all times solely derived from the state of the model. This leads to a wall between the M, V, and C components. Each one has a clear API and doesn't interfere with the others. In fact, in Elm's Architecture documentation, each piece has a concrete type. The extreme regularity of this approach even means that the architectural pattern itself can be a library in Elm.

By contrast, when mutability is involved, the siren call of that "feature" inexorably leads to one cheating. In practice, it's too difficult to firewall the layers of MVC from one another. Mutability is a kind of Chekhov's gun, it will go off by the third act simply because it is there.

An Example App: Elmchat

I wrote an example application to try all this out for myself. Elm comes with very good examples, but I tend to learn best by doing. I also wanted to see how to communicate with a JSON API in a realistic(-ish) scenario. What I came up with was a chat app. There's a form and a list of current messages (maintained by the server). Updates are accomplished through polling every 5 seconds. This is not optimal and in a more robust application I'd update the messages in real-time over websockets. But it did serve my goal of writing for a RESTful JSON API, which I use much more IRL – square peg, meet round hole. As an aside, since my focus was really the frontend, I used PostgREST to serve a (very) simple DB schema; highly recommended.

Let's take a look at the code.

Following the Elm architecture, I've split my code into 3 main parts: Model.elm, View.elm, and Update.elm. There are some supporting files that define things like type aliases and the API wrapper. All these are brought together in the Main.elm file.

Let's start with Main, at a high level, and then dive into each of it's dependencies.

(Part of) Main

-- ...omitting imports & etc.

main : Signal Html
main =
  Signal.map (view actions.address) mergedModel


actions : Mailbox Action
actions =
  Signal.mailbox NoOp


mergedModel : Signal Chat
mergedModel =
  Signal.foldp update model mainSignal


mainSignal : Signal Action
mainSignal =
  mergeMany
    [ actions.signal
    , currentMessages.signal
    , outgoingMessages.signal
    ]

--- ...omitting other helper functions

The main function ties together the whole model, view, update trio; although a bit indirectly. In main, the view function is partially applied to an Address of Actions. In Elm, an Address is something like a "channel" where messages can be sent. In this case the messages are user-actions (see types). Once partially applied, view actions.address is then a function expecting a model (type Chat) argument. That's where the Signal.map comes in. We have a signal of mergedModel, which says "here are all the changes to the model over time (e.g. "the user name is now set")." Signal.map adapts our view actions.address function to accept a signal of model changes! Since the view function finally yields an Html type, our whole main function is a Signal Html, that is, Html which changes over time! A pretty apt description of a modern webpage.

Diving into the other functions, mergedModel is a Signal of model updates resulting from applying the update function on each Action that arrives, in mainSignal.

Finally, mainSignal merges Signals coming from the UI, actions.signal, from the API server, currentMessages.signal, and the outgoing message signal, outgoingMessages.signal, (POSTs to the API).

Model

module Model where


import Types exposing (Chat, Message)


model : Chat
model =
  { messages = []
  , field = ""
  , name = ""
  }

The model code may be the most shocking of all depending on your MVC background. Here, the model really is only a container for data. We model three fields, messages, a list of Messages, the current input field, and an input field for the user's name. The last two inputs are both of type String.

Think of all the M-things that this model does not do. There's no storage, no syncing, and no formatting or presentation code. This is purely a data structure.

Types

module Types where


import Array exposing (Array)


type Action
  = SendMessage Message
  | Input String
  | SetName String
  | Incoming (List Message)
  | NoOp


type alias Message
  = { name: String, message: String }


type alias Chat =
  { messages: List Message
  , field: String
  , name: String
  }

The types module is a place where I defined application-specific types to use throughout the rest of the code. Putting this in its own module simplifies imports. It also serves as excellent documentation. The app is basically described by the Action union type. These are all the actions that a user takes inside the application. An Action is exactly one of:

  • SendMessage – This indicates a message to be sent to the REST endpoint, the user has added a message.
  • Input – This is when the user has typed something into the chat input, each character is added to the model as it is typed. The model thus always represents what's currently in the chat input.
  • SetName – This action works similarly to Input, but means that the user has typed into the name input.
  • Incoming – This Action is the result of messages coming back from the REST endpoint. The model will be set to the current list of messages.
  • NoOp – It is occasionally useful to have an action that "does nothing." Notably, it is used as a kind of "blank" initial state.

Together you can see that these messages represent the abstract actions that a user can take in the application. What's so powerful about this technique is that the actions themselves are just data. These are not functions or methods, it is entirely up to another function to give them meaning. You'll see in the next section that the update function acts as an interpreter of this data structure.

Update

module Update where


import Signal exposing (Address)


import Types exposing (Action(..), Chat)


update : Action -> Chat -> Chat
update action model =
  case action of
    SendMessage msg ->
      { model | field <- "" }

    Incoming msgs ->
      { model | messages <- msgs }

    Input say ->
      { model | field <- say }

    SetName name ->
      { model | name <- name }

    NoOp -> model

update acts as a central dispatch for the incoming Actions. The action is combined with the current state of the model to yield a new model. This is the folding function, foldp, that is used in Main.mergedModel to "step" time forward in the app.

The notation above, { model | field <- newValue }, is an update. It says that we're creating a new value from model where the current value of field is set to newValue. So we can see how each of the Actions affects the model. Notice that SendMessage merely blanks out the current field. When the user hits enter to send a message, their message disappears, only to appear a second or two later in the chat log. This is because the SendMessage action is sent to an Api.outgoingMessages address (Not shown, but if you're interested, the code is here).

(Part of) View

-- ...imports omitted
view : Address Action -> Chat -> Html
view address model =
  container_
  [ stylesheet "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"
  , stylesheet "css/style.css"
  , node "link"
    [ A.href "http://fonts.googleapis.com/css?family=Special+Elite"
    , A.rel "stylesheet"
    ]
    []
  , img [A.src "images/joan.png"]
    []
  , h1_ "Can We Talk!?"
  , row_ [ inputControls address model ]
  , row_ [ messageList model ]
  ]
-- ...omitted other view & helper functions

Much of the view code is omitted here, but you should still be able to smell the general structure. The layout is created by a bootstrap grid container_ and row_. Each of these tags is just a normal function, stylesheet is one that I wrote myself.

Both the inputControls and messageList functions are implemented probably how you'd imagine, they set up a form for input and a table for message output, respectively. The big thing to notice here is that the view is literally a function of an Address to send updates to and the current state of the model. That's it. We don't have to go chasing anything else that might affect the template.

Notice also how nice and compositional elm-html lets us write. If we ever find a repetitive segment of HTML, we can pull it out into its own function. Don't underestimate how effective this technique is. I did that here for the inputControls and messageList functions. I also used elm-html-shorthand to be able to write the more succinct tag_-style html functions.

And that wraps it up. That's (almost) the whole application. Go look at the full code, what I have above is narrowed to only the interesting bits.

I'm still getting my 10,000 lines of code in with Elm, but I can already see that it has many nice features. But this is really a sum-is-greater-than-the-parts situation. The things included and omitted from Elm-the-language set it apart from other frontend solutions that I've seen. I've picked out a few things that are representative of the choices that Elm makes. Let's look at them.

What Elm Brings to the Table

Like I said, this is a partial list. Elm has a lot of other nice features. Go check out the syntax overview for a broad look how Elm works. But here are some things that deserve special mention.

An Amazing Package Manager

This is the only package manager I know of that enforces semver. The Elm package manager computes an API-sensitive diff from the previous code and applies a few rules to determine the next version. This can result in three things:

  1. If all functions have the same type signatures and there are no new ones, that means this is a PATCH version.
  2. If all the previously-existing values are the same, but new ones have been added (i.e. forward-compatible) this is a MINOR version.
  3. If any of the current API has changed or been removed, this is a MAJOR version.

The docs summarize it as follows:

This means that if your package works with evancz/elm-html 1.2.1 it is very likely to work with everything up until 2.0.0. At that point, some breaking change has occurred that might break your code. It is conceivable that things break on a minor change if you are importing things unqualified and a newly added value causes a name collision, but that is not extremely likely.

And that's it. Finally, version numbers mean something for real!

Extensible Records

Don't be fooled by the dry term above! In most popular dynamic languages people talk about something called duck typing The term is somewhat fuzzy in general, but the gist is that you can index into an object and if the object supports that message/call it'll work, regardless of the "type" of the object. Extensible records are static duck typed. That is, within a function we can make this kind of call:

a = { first = "John", last = "Doe" }
b = { first = "Jane", last = "Doe", age = 33 }
c = { first = "Jay", age = 40 }

collate person =
  person.last ++ ", " ++ person.first

collate a -- "Doe, John"
collate b -- "Doe, Jane"
collate c -- Does not typecheck! (Missing "last" field)

I feel that this gives you a lot of the flexibility of dynamic languages, while keeping the benefits of static typing. Win-win.

JavaScript Interop

JavaScript interop, which Elm calls JavaScript Foreign Function Interface (JS FFI), was described in the podcast as "JavaScript as a service." And that's how it works. Elm can interact with any JS you want. To send messages out to JS-land you declare a port and subscribe to it in JS:

-- outgoing values (in Elm)
module Example where

port time : Signal Float
port time = every second
// subscribe in JS
var example = Elm.worker(Elm.Example, { /* initial values */ });
example.ports.time.subscribe(callback); // callback is called every second with the time

In the other direction, you can send values into Elm from JS:

-- incoming value (Elm)
port prices : Signal Float
// outgoing value (JS)
var example = Elm.worker(Elm.Example, { /* initial values */ });
example.ports.prices.send(42);

You can see that the values are exposed as a Signal of values inside Elm. This fits because the JS code may send further updates (e.g. example.ports.prices.send(12)). And in the JS direction, the structure is that of a callback that's called whenever the Signal has a new value. Elm parses the values coming in from JS to ensure that they are well-typed.

Stellar Error Messages

Elm has done great work on making error messages "for humans." It pays off big. Here is an example taken from the linked blog post:

Example of Elm error message

The error highlights code, as written, at the exact spot, with rationale, and suggestions for fixes. Paired with type inference these excellent error messages really get to the root of problems. Working in this style is "you have to try it" territory, but I think you'll be glad you did. Catching bugs right away (and not weeks later as a bug report) is so refreshing.

Summary

Elm, even though it is new, even though it is different, offers a real and substantial alternative to MV*-style frameworks like Angular and Ember. Code in Elm is cleaner, more maintainable, and simpler than the alternatives. It improves upon JS by nearly eliminating runtime crashes, meaning no more "undefined is not a function."

Instead of CSS and HTML, you get a composable, programmable layout language that really works. I sat in a presentation on Elm back at the 2012 Emerging Languages Camp. During his presentation, Evan Czaplicki, Elm's author, vertically centered a page element and the room cheered. The message I got was clear, "this is how weak our current tools of HTML and CSS are."

The landscape of innovation on the frontend is wide open. The dominance of mutable, imperative, OO-style frameworks is not predestined. JavaScript is as much (or more) a functional language as anything else. We can use innovative techniques that we've learned in the time since the early 90's (JavaScript, Java, Ruby, Python, etc.) to build better stuff. Let's do it.