Elm for the Frontend, Right Now (Updated for Elm 0.18)

The following article was originally written in July 2015 for the 0.16 release of the Elm programming language. It has been rewritten here in November 2016 to comply with the syntactic and semantic changes of Elm 0.18. Enjoy!


The Changelog recently did a podcast (#191) 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.

(Most of) Main

-- ...omitting some imports & etc.
import Model exposing (Chat, model)
import Update exposing (update)
import View exposing (view)

init : (Chat, Cmd Msg)
init = (model, prefetchMessages)

prefetchMessages : Cmd Msg
prefetchMessages =
  Task.perform
    (always PollMessages)
    (Task.succeed ())

subscriptions : Chat -> Sub Msg
subscriptions _ =
  every (5 * second) (always PollMessages)


main : Program Never Chat Msg
main =
  Html.program
    { init = init
    , update = update
    , view = view
    , subscriptions = subscriptions
    }

The main function ties together the whole model, view, update trio, plus a way to subscribe to external events ("every time the clock ticks five seconds," in this example). The call to Html.program takes one record with 4 key-value pairs. The init value is is the only one of the four that should just be data, rather than a function. It expects a two-element tuple with the initial model and a command to run immediately. In our case, the model is just the default (blank) model returned by a call to model. Let's take a deeper look at the model, and then we'll consider the three functions: update, view, and subscriptions.

Model

module Model exposing (..)

import RemoteData exposing (RemoteData(NotAsked), WebData)

-- Main Model

type alias Chat =
  { messages : ChatList
  , saying : Saying
  , name : Name
  }

model : Chat
model =
  { messages = NotAsked
  , saying = ""
  , name = ""
  }

-- Pieces

type alias ChatList = WebData (List ChatMessage)
type alias Name = String
type alias Saying = String

type alias ChatMessage =
  { name : Name
  , message : Saying
  }

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 just three fields. First is messages, a ChatList, which is an alias of "a WebData of a List of ChatMessages." The WebData wrapper is courtesy of the RemoteData package, and it wraps the underlying value in one of four states: NotAsked, Loading, Failure Http.Error, and Success a (where a is the class you're wrapping). This multiplexing of 4 states of your data makes it much easier later to indicate to the user what is going on.

The second and third fields, saying and name are pretty self-explanatory. They're both a String and they'll both be tied to text fields, though they got their own special types, mostly so we can keep track of which is which. You could imagine flipping the order of the "name" and "message" fields in the user interface, only to find that you were depending on the original order of Strings in your code. At that point, you'd have two options: either refactor your entire codebase to flip the order everywhere, or deal with the cognitive dissonance that the ordering in the UI layer is different than the ordering everywhere else.

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.

Messages

module Messages exposing (Msg(..))

import Model exposing (..)

type Msg
  = SendMessage Name Saying
  | Incoming ChatList
  | Input String
  | SetName String
  | PollMessages

The Messages 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 Msg union type. These are all the actions that a user takes inside the application. A Msg is exactly one of:

  • SendMessage – This indicates a message is to be sent to the REST endpoint because the user has added a message.
  • Incoming – This Msg is the result of messages coming back from the REST endpoint. The model will be set to the current list of messages.
  • 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 Msg works similarly to Input, but means that the user has typed into the name input.
  • PollMessages – This gets triggered only by our subscription to the clock, and means that we'd like to get a new set of data from the server.

Together you can see that these messages represent the abstract actions that a user (and the app's subscriptions) can take in the application. What's so powerful about this technique is that the messages 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 exposing (update)

import Api
import Messages exposing (Msg(..))
import Model exposing (Chat, ChatMessage)

update : Msg -> Chat -> (Chat, Cmd Msg)
update msg model =
  case msg of
    SendMessage name saying ->
      let
          message = ChatMessage name saying
      in
         ({ model | saying = "" }, Api.sendMessage message (always PollMessages))

    Incoming msgsResult ->
      ({model | messages = msgsResult}, Cmd.none)

    PollMessages ->
      (model, Api.fetchMessages Incoming)

    Input say ->
      ({ model | saying = say }, Cmd.none)

    SetName name ->
      ({ model | name = name }, Cmd.none)

update acts as a central dispatch for the incoming Msgs. The msg is combined with the current state of the model to yield a new model and a command. If you're familiar with the reduce, inject, or foldl functions in other languages, you can consider the update method to be the body/accumulator you pass into those functions: "the memoized value plus a new thing gives a new memoized value," though here we also insert the ability to kick off asynchronous jobs with the Cmd Msg.

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 Msgs affects the model. Notice that, as far as the model is concerned, SendMessage merely blanks out the current field. When the user hits enter to send a message, their message disappears, only to appear a split second later in the chat log. This is because the call to update with a SendMessage message also returns a command returned by the Api.sendMessage function (not shown, but if you're interested, the code is here).

(Part of) View

-- ...imports omitted
view : Chat -> Html Msg
view model =
  div [ class "container" ]
    [ stylesheet "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"
    , stylesheet "css/style.css"
    , stylesheet "http://fonts.googleapis.com/css?family=Special+Elite"
    , row_
      [ h1  [class "col-xs-7"] [ text "Can We Talk!?" ]
      , div [class "col-xs-5"] [ img [src "images/joan.png"] [] ]
      ]
    , row_ [ errors model.messages ]
    , row_ [ inputControls model ]
    , row_ [ Lazy.lazy messageList model.messages ]
    ]

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 a Chat model and returns some Html that knows how to send Msg messages to the Elm subsystem. 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 wrapped the messageList function call inside of Lazy.lazy, which tells Elm's HTML rendered to skip redrawing that section if the second argument—model.messages—didn't change.

messageList : ChatList -> Html a
messageList messages =
  case messages of
    NotAsked ->
      notTable "Loading..."
    Loading ->
      notTable "Loading..."
    Failure e ->
      notTable "Loading failed"
    Success s ->
      let
          messages_ =
            s
              |> List.reverse
              |> List.take 30
              |> List.map msgRow
      in
        zebraTable
          [ th [ class "col-xs-2" ] [ text "Name" ]
          , th [] [ text "Message" ]
          ] messages_

Taking a closer look at the body of messageList, we see that we're unwrapping the WebData structure around our List of ChatMessages. Because we can't just start iterating over our ChatMessages without unwrapping it first, we have to write a case .. of statement. And since Elm forces us to handle every situation of WebData, we now have explicit points in our code to handle what to do when: a) we haven't asked for the data yet, b) the data has been requested but hasn't yet arrived, c) we received an error, and d) we finally have the data. In the first 3 cases, we show some HTML that is… well… not a table, and when we finally do have valid data, we show a zebra table with headers.

And that wraps it up. That's (almost) the whole application, less the API bits. 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 elm-lang/html 1.1.0 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 compile/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 is effectively "JavaScript as a Service:" Elm can interact with any JS you want. If you're already familiar with JavaScript's Web Workers, this will seem quite familiar. To send messages out to JS-land you declare a port and subscribe to it in JS (while also declaring your whole module as a port module):

-- outgoing values (in Elm)
port module Main exposing (..)

-- ...

-- port for sending events to an analytics package
port analyticEvent : String -> String -> Cmd msg
-- that's it! no need to implement anything else in Elm-land
// initialize the Elm app (JS)
var app = Elm.Main.fullscreen();

// subscribe in JS
app.ports.analyticEvent.subscribe(function(category, action) {
  ga('send', 'event', category, action);
});

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

-- incoming value (Elm)
port module Main exposing (..)

-- ...

-- port for listening to a "done" event from a date-picker
port datePicked : (List String -> msg) -> Sub msg
// initialize the Elm app (JS)
var app = Elm.Main.fullscreen();

// outgoing value
$('.datepicker').datepicker()
  .on('hide', function(e) {
    app.ports.datePicked.send(e.dates);
  });

You can see that the values are exposed as a Cmd of values inside Elm. This fits because we effectively want the command to execute asynchronously, this time in JS land instead of Elm land. And when values come back from the JS code, the structure is that of a subscription that's triggered whenever the port receives 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.

Discuss this article on Hacker News and Lobste.rs.

Have a tough project that could use the expertise of a team like Bendyworks? Get in touch!