Azer Koçulu May 21, 2018

The Hitchhiker's Guide to Elm

Perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away. — Antoine de Saint-Exupery

Back in 2006, front-end development was simple and imperfect. We were really excited about building apps on web browsers. Amazing stuff like EditGrid and Netvibes were built. It's been 12 years since then, the way we build web apps has changed quite significantly because we've been hoping that incremental improvements will fix fundemental issues with JavaScript, HTML and CSS.

And incremental improvements made front-end development quite messy. Developers waste hours bruteforcing tools and libraries until they can finally work together. We have excellent developer tools, but error messages are mysterious. The community can never agree on minor style issues. There is lots of ways to solve problems, but none is simple, scalable and productive at same time.

It's time to start benefiting from the advances in programming language design.

It was 2012 when a Harvard student named Evan Czaplicki designed a new, functional reactive programming language called Elm as his thesis project. He also implemented this language and shared with rest of the world. Elm gained some popularity since then, while inspiring JavaScript community to build libraries such as Redux.

Elm syntax doesn't look like JavaScript, so you might be afraid of learning curve. In fact, Elm is smaller and simpler than JavaScript, and it shouldn't take longer than 8 hours to go through all this documentation, including doing some exercises. I belive it's a good investment, plus it's fun.

1. What is Elm?

Elm is a statically typed functional programming language specifically designed for building web apps. It's really fast, and got an excellent compiler that makes it possible to have no runtime errors. There is no undefined or null. Its core language is minimalistic and simple that you can walk through in half an hour.

Example Code

Before exploring Elm more in the further sections, here is a piece of Elm and JavaScript/HTML code side by side. Both code do the same job; we define a function named double, it returns the double of given argument n. We print result of 5 * 2 to the screen.

module Double exposing (main)

import Html exposing (text)

double : Int -> Int
double n = n * 2

main =
    text (toString (double 5))
<html>
  <body>
    <script type="text/javascript">
    function double (n) {
      return n * 2
    }

    document.body.innerHTML = double(5)
    </script>
  </body>
</html>

Why is it different?

Elm is not an evolution of JavaScript; it's a holistical alternative for the whole JavaScript ecosystem. Instead of being an incremental improvement, Elm designs a GUI development experience that is simple, straightforward and robust.

While type annotation is optional, Elm can infer all the types and its compiler catches errors in the compile time by providing you very specific and human friendly compile errors. In the other words, errors happen in the compile time in front of the developer, instead of runtime, in front of users.

Here is an example error message when we misspell double as doule:

$  elm-make double.elm
-- NAMING ERROR ----------------------------------------------------- double.elm

Cannot find variable `doule`

9|     text (toString (doule 5))
                       ^^^^^
Maybe you want one of the following?

    double

Detected errors in 1 module.

Tools

Elm provides a standard toolset that gets versioned and shipped together. Below is an overview of standard tools and a few extra tools that are de-facto standars in the community;

Tool Desc Core Community
elm-make Compiler
elm-package Package manager. Example Package
elm-format Code formatter. No config files, no minor style issues.
elm-reactor A local server auto-compiles elm files.
elm-test Testing library
elm-html HTML rendering library backed by virtual-dom.
elm-css CSS library and compiler

Install

Before we start coding, let's make sure you've got Elm installed in your system;

After installation, create a folder named elm-sandbox in your home folder and open up elm-repl to run following code:

> String.reverse "I love tea!"

Works? Perfect. elm-repl is useful for experimenting the language basics. Use it for all short examples in the language reference. Now open up another terminal window and cd into the elm-sandbox folder you've created. This time, run elm-reactor command. Once elm-reactor is running, open your personal code editor and save following code into the sandbox as hello-world.elm:

import Html exposing (text)

main =
    text "Hello world"

Now open up localhost:8000/hello-world.elm. Do you see the "Hello world" ? If yes, you're ready to rock. If not, please make sure you've followed the steps correctly.

2. The Language

In this section we'll explore Elm language. Make sure you've completed the installation steps explained above.

Functions

Here is how we define a simple function called multiply; it takes two arguments and returns multiplication of them:

> multiply x y = x * y

There is no return statement because each expression will result in a value. Let's call the function we've just defined and see the result:

> multiply 2 5
-- 10 : number

Partial Application

You can create partial functions easily. Let's define a function that multiplies given number by 2, by using the multiply function we defined above:

> multiplyBy2 = multiply 2

Elm creates a partial function if all arguments are not passed. This makes Partial Application really easy:

> multiplyBy2 5
-- 10 : number

Pipelines

Multistep data operations can be done using pipelines. Here is comparision of same code with pipelines and without:

-- With pipelines

reverseUppercase text =
    text
        |> String.toUpper
        |> String.reverse
-- Without pipelines

reverseUppercase text =
    (String.reverse (String.toUpper text))

They'll both return MLE when we call them like reverseUppercase "Elm". We can define reverseUppercase using function composition, too. Check it out below.

Function Composition

Function compositions are useful when we want to create a new function from other functions. In Elm, it can be done using << (right to left) and >> (left to right) operators. While pipelines require values and return values, function composition only require functions and return functions as a result.

Here is the comparision of same code with function composition and without:


-- With function composition

> reverseUppercase = String.toUpper >> String.reverse
-- Without function composition

reverseUppercase text =
    (String.reverse (String.toUpper text))

Above code creates a new function that takes a parameter, passes it to toUpper first. Then the result of toUpper gets passed to reverse and whatever reverse returns is the result of the function we've defined.

Let

let expression allows us define variables inside functions. Here is an example:

someRandomMath x =
  let
    y = 5
    z = 10
  in
    (x + y) * z

Let's call the function we've just created:

someRandomMath 4
-- 90 : Number

If Expressions

It probably looks quite familar to you;

above100 n =
    if n > 100 then
        "Yeah, it is above 100"
    else
        "Nope, it's not"

The function we've defined takes an argument, checks if it's bigger than 100 and returns a string. No return statement needed, the function naturally results in a value.

Lists

Lists hold collection of same type of values. You can define them with brackets and use the functions in List package:

> numbers = [ 3, 1, 2 ]

> List.sort numbers
-- [1, 2, 3] : List number

You can manipulate list elements using the map method. The result will be a new list:

> double n = n * 2

> List.map double numbers
-- [6, 2, 4]

Tuples

Tuples hold fixed number of values; but they can be any type.

> (True, "Hey there")

This is very helpful for returning multiple values from a function. The most common use case you'll see is the update functions every Elm program has. They return a new model and a command every time an update happens in the program;

update : Msg -> Model -> (Model, Cmd Msg) -- It takes Msg and Model, returns Model and Cmd Msg
update msg model =
  PlayMusic ->
            ( { model | playing = True }, Cmd.none )

Records

Records are equivalent of JavaScript objects in Elm. Here is how we can define a simple record:

> kanye = { name = "Kanye West", children = 2 }

We all know Kanye and Kim had a new child recently and we need to update that record. Easy-cheesy:

> newKanye = { kanye | children = 3 }

Two things to notice in the above code:

You can access value of a record field with . followed by field name:

> .name kanye
-- Kanye West : String

> .children kanye
-- 2 : Number

As you may guess, .children above is a function even if we didn't define. So we can use it with other functions that works with functions, List.map for example:

> lilwayne = { name = "Lil Wayne", children = 0 }

> List.map .children [kanye, lilwayne]
[3,0] : List number

Elm supports destructuring, too. Here is how we can benefit from destructuring;

hasChildren rapper =
    let
        { children } = rapper
    in
        children > 0

You can define a function that destructures given parameter:

> hasChildren { children } = children > 0

> hasChildren newKanye
-- True : Bool

These are useful, we might need to define an alias for this record type though. Let's check how types work in Elm;

Types

Type Aliases

Type aliases give alternate name for an existing type. For example;

type alias Text = String

Now Text is an alternate name for String. A very common use of type aliases is to give record types a name:

type alias Rapper =
  { name: String
  , children: Int
  }

The Rapper type alias we've defined is just an alias for { name: String, children: Int }. It's obviously more convenient to create aliases for record types that we use in multiple places in the codebase.

Type aliases are constructors at same time;

drake = Rapper "Drake" 0
-- { name = "Drake", children = 0 } : Rapper

Let's define hasChildren again, using type annotation this time:

hasChildren : Rapper -> Bool
hasChildren rapper = rapper.children > 0

Union Types

A union type specifies exactly what values it can have. For example;

type AudioEvent
    = Play
    | Pause
    | VolumeChange Int

We've just defined a new type named AudioEvent and it can only have specified three values. Values might have their attachment, too. For example,VolumeChanged expects an Int value to be passed along.

Want to see these values being used in a practical example? See the next section; case-of.

case-of

Union types and case-of constructs are the most powerful features of Elm. They let us define a complex conditions and handle them naturally.

Here is how we can use AudioEvent type we've defined in the previous section:

onAudioEvent : AudioEvent -> String
onAudioEvent event =
    case event of
        Play ->
          "Starting music..."

        Pause ->
           "Pausing music..."

        Volume n ->
           "Changing volume to " ++ (toString n)

We'll benefit from union types and case-of in the most critical parts of our Elm applications.

3. Let's Build Apps!

Every Elm program consists of model, view and an update function that handles all the updates to the model. What does this mean ? Let's build a few examples to understand it.

Radio Player

We want to build a little radio player. It will have two buttons: Play and Pause. Whenever user clicks play, we'll start playing Radio Paradise.

Every Elm program starts by defining types. We'll use type alias for defining the model, and union type for defining the messages:

type alias Model =
    { playing : Bool
    , src : String
    }


type Msg
    = Play
    | Pause

Now we have an idea about what this program does. Model describes its data model, and Msg describes the external events that our program should react to. The next step is to create the update function, which implements the changes we've defined.

update : Msg -> Model -> Model -- Update function takes `Msg` and `Model` as parameters, and returns a new `Model`.
update msg model =
    case msg of -- What type of Msg we received ? Play or Pause ?
        Play ->
            { model | playing = True } -- If the message is play, set the playing field as True.

        Pause ->
            { model | playing = False } -- See `Records` section if this syntax looks weird to your eyes.

We defined the types, implemented the update function that handles the Msg variations that could be sent from an external source. Time to build the user interface.

Every Elm program needs a view function that takes Model and returns Html.

We use the core Html package to create the view. Please run elm-package install elm-lang/html command in your project folder to get this package installed.

Here is a simple example of creating HTML elements:

import Html exposing (h1, text)
import Html.Attributes exposing (class)

main =
  h1 [class "title"] -- Attribute list
     [text "Hello World"] -- Children elements list
<html>
  <body>
    <h1 class="title">
      Hello World
   </h1>
  </body>
</html>

The actual view function we'll code is little bit more complex than the above example; it should return two different interfaces; play button when the music is stopped, pause button and audio elements when the music is playing. We'll have the condition in the main view function, and create two more functions to implement the views:

view : Model -> Html Msg
view model =
    if model.playing then
        playingView model
    else
        notPlayingView model

notPlayingView : Model -> Html Msg
notPlayingView model =
    button [ onClick Play ] [ text "Play" ]

playingView : Model -> Html Msg
playingView model =
    div []
        [ button [ onClick Pause ] [ text "Pause" ]
        , audio [ src model.src, autoplay True, controls True ] []
        ]

Our program is almost ready to run, it's missing the main function every Elm program needs;

main =
    Html.beginnerProgram
        { view = view
        , model = Model False "http://stream-tx4.radioparadise.com/mp3-192"
        , update = update
        }

Elm reads the whole file before executing, so you can refer to variables defined later. It's a common practice put the main on top of your file.

Once you save your code, compare what you've got with full code of this example.

Run / Compile

Ready to see your code working? Run elm-reactor in the project directory and open localhost:8000. You can make changes and refresh your browser to see the changes.

Once you finalize the changes, you can either compile HTML or JavaScript using elm-make. I personally prefer creating my own HTML and injecting compiled Elm code in:

$ elm-make radio.elm --output radio.js

The compiled JavaScript file can be injected to anywhere in the DOM:

<html>
  <body>
    <div id="elm"></div>
    <script type="text/javascript" src="radio.js"></script>
    <script type="text/javascript">
      Elm.App.embed(document.querySelector("#elm"))
      // P.S `Elm.App` path can be different depending on the package name.
    </script>
  </body>
</html>

We got a simple app working already, wow! Now I assume you want to make your application look pretty. We'll learn how to style our Elm programs with CSS.

CSS

elm-css allows us style our programs easily. We can define all the CSS properties inline in the view functions, they'll get compiled and added into the DOM tree inside style elements automatically.

Here is a simple example and its output:

module Main exposing (..)

import Css exposing (..)
import Html.Styled exposing (h1, text)
import Html.Styled.Attributes exposing (css)


main =
    Html.Styled.toUnstyled title


title =
    h1
        [ css
              [ backgroundColor (rgb 255 200 50)
              , padding (px 20)
              , textAlign center
              , fontSize (em 3)
              ]
        ]
        [ text "Hello World" ]
<!DOCTYPE html>
<html>
  <body>
    <h1 class="_57c3f2a9">
      <style>
        ._57c3f2a9 {
          background-color: rgb(255, 200, 50);
          padding: 20px;
          text-align: center;
          font-size: 3em;
        }
      </style>
      Hello World
    </h1>
  </body>
</html>

There is a few important lines we need to pay extra attention in the above example:

HTTP Requests & JSON Parsing

We all make API requests and they're often JSON. In this imaginary Elm program, we'll pull list of songs from a radio API.

First of all, we need to define the kind of response we expect:

type alias APIResponse =
    { songs : List String }

Secondly, we need to create (or append) a Msg value for the HTTP event.

type Msg = APILoaded (Result Http.Error APIResponse)

Now we're ready to define how we'll make the request and parse the response.

import Json.Decode as Decode
import Http

sendRequest : Cmd Msg
sendRequest =
    Http.send APILoaded (Http.get "/api/songs" responseDecoder)

responseDecoder : Decode.Decoder APIResponse
responseDecoder =
    Decode.map APIResponse
        (Decode.field "songs" (Decode.list Decode.string))
{
  "songs": [
    "Porcupine Tree — I Drive The Hearse",
    "Tom Waits — Hold On",
    "Florence + The Machine — Leave My Body",
    "The Eagles — Seven Bridges Road"
  ]
}

The above code makes a GET request to '/api/songs' path, and raises APILoaded event passing the parsed response returned from responseDecoder. Now we can handle APILoaded message in the update function:

update msg model =
    case msg of
       APILoaded (Ok response) ->
         { model | songs = response.songs }
       APILoaded (Err err) ->
         { model | error = (toString err) }

Init

init functions define the initial model and the commands that should be executed immediately. Imagine a case you need to make an API request to pull the content of a page, init function is where you make that initial request;

init : ( Model, Cmd Msg )
init =
    ( Model [], sendRequest) -- See HTTP chapter above for definition of sendRequest

You can execute multiple commands using Cmd.batch;

init : ( Model, Cmd Msg )
init =
    ( Model [], Cmd.batch (List.map sendRequest ["foo", "bar"]))

If you prefer not to execute any commond, use Cmd.none:

init : ( Model, Cmd Msg )
init =
    ( Model [], Cmd.none)

In the earlier radio player example we've used Elm.beginnerProgram. In rest of the examples we'll use Elm.program instead. It requires us to provide subscriptions which we'll cover after init.

Here is an example main function using Elm.program:

main : Program Never Model Msg
main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = always Sub.none
        }

Subscriptions

Subscriptions allow Elm programs to listen external input such as browser events, timers, port messages.

In the following example, we'll listen for keyboard events. If user presses space key (KeyCode 32), we'll make a change in the model. (P.S elm-lang/keyboard package is required.)

import Keyboard

type Msg = Keypress Keyboard.KeyCode

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
      Keypress code ->
        if code == 32 then
          ( { model | muted = True }, Cmd.none )
        else
          ( model, Cmd.none )

subscriptions : Model -> Sub Msg
subscriptions model =
    Keyboard.downs Keypress

Ports

We can define ports for both sending and receiving messages between Elm programs and external JavaScript code. Imagine a case our program needs to interact with a web worker written in JavaScript. We need to define a port for sending messages from Elm to JavaScript, and another port for receiving message from JavaScript;

Following example defines the type of what we'll receive, creates the ports and subscribes to the incoming port.

port module MyProgram -- Modules with ports has to declare it.

type Msg = NewMessageToElm String

port toJS : String -> Cmd msg
port toElm : (String -> msg) -> Sub msg

subscriptions : Model -> Sub Msg
subscriptions model =
    toElm NewMessageToElm

update : Model -> Msg -> (Model, Cmd Msg)
update msg model =
  Case msg of
    NewMessageToElm str
      ({ model | messageFromJS = str }, Cmd.none)
<html>
  <head></head>
  <body>
    <script type="text/javascript" src="myprogram.js"></script>
    <script type="text/javascript">
     const myProgram = Elm.MyProgram.fullscreen()

     myProgram.ports.toJS.subscribe(function (msg) {
         console.log('[elm-to-js]', msg)
     })

     myProgram.ports.toElm.send("Hey there")

    </script>
  </body>
</html>

See the following section, Example 2: Fake chat for a working example using ports.

Example 2: Fake Chat

This time we'll create a fake chat app to exercise what we've learnt in last sections. Our program will;

As always, we start coding types first:

port module FakeChat exposing (..)

type alias Model =
    { input : String
    , messages : List String
    }


type Msg
    = InputChange String
    | SendToJS
    | NewMessage String

We need two ports, incoming (toElm) and outgoing (toJS).

port toJS : String -> Cmd msg
port toElm : (String -> msg) -> Sub msg


subscriptions : Model -> Sub Msg
subscriptions model =
    toElm NewMessage

In the subscriptions functions above our program started listening messages from the toElm port. When JS program sends a message, our updater will receive NewMessage String.

update function is the key part of this program as usual. In the below code we'll handle SendToJS messages which gets sent when user presses the submit button. In return, we'll send a command created by toJS port. This is all we need to send messages to JavaScript.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        InputChange newInput ->
            ( { model | input = newInput }, Cmd.none )

        SendToJS ->
            ({ model | input = "" }, toJS model.input)

        NewMessage incoming ->
            ({ model | messages = model.messages ++ [incoming] }, Cmd.none)

Now we need to get the views. Our Elm program will be a minimalistic interface with list of messages and an input. Every time the value of input changes, it'll fire InputChange message and our updater we've defined above will set model.input to the new input value.

view : Model -> Html Msg
view model =
    div []
        [ viewMessages model.messages
        , input [ type_ "text"
                , placeholder "Type a message"
                , onInput InputChange
                , value model.input
                ] []
        , button [ onClick SendToJS ] [ text "Send to JS" ]
        ]


viewMessages : List String -> Html Msg
viewMessages messages =
    ul []
        (List.map viewMessage messages)


viewMessage : String -> Html Msg
viewMessage message =
    li []
        [ text message ]
<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <script type="text/javascript" src="ports.js"></script>
    <script type="text/javascript">
     // `fullscreen` is a shorthand function for inserting Elm interface into the DOM
     const fakeChat = Elm.FakeChat.fullscreen()
     const names = ["Alice", "Bob", "John"]

     // Start listening messages sent from elm
     fakeChat.ports.toJS.subscribe(function (msg) {
         console.log('[elm-to-js]', msg)
         const name = names[Math.floor(Math.random() * names.length)]

         // Append a random nickname in front of the message, send it back to Elm
         fakeChat.ports.toElm.send(`<${name}> ${msg}`)
     })

    </script>
  </body>
</html>

Compile this program with elm-make and include it in an HTML page as you see in the above example. Compare your code with full working example if you face any unexpected problems.

Codebase Structure

This part of the guide is possibly misleading beginners. Elm is a functional language and it's recommended to not create components that contains state. Checkout sortable table example for the recommended approach on components.

While we can create quick prototypes in one-file Elm programs, we'll want to modularize the codebase into smaller parts in the real world. A good practice is to split your app into folders, then split every folder into different files (Types, State, View etc...).

Recently I built a radio player app (you can actually try it). It consists of four isolated components:

Each component consists of three common modules;

Some components have more modules depending on what they do. The final directory layout of the radio player I mentioned is following:

──  elm-package.json
──  src
    ├── main.elm
    ├── Container
    |   ├── State.elm
    |   ├── Types.elm
    |   ├── View.elm
    ├── History
    |   ├── Rest.elm
    |   ├── State.elm
    |   ├── Types.elm
    |   ├── View.elm
    |   ├── Style.elm
    ├── Player
    |   ├── Events.elm
    |   ├── Icons.elm
    |   ├── State.elm
    |   ├── Types.elm
    |   ├── View.elm
    |   ├── Style.elm
    ├── Wallpaper
        ├── State.elm
        ├── Types.elm
        ├── View.elm

Container Components

Container components not only put views together, they also call init functions of every component, distribute messages correct component's update function, and get subscriptions started.

We keep our application state unified, so our Model consists of the child models:

module Container.Types exposing (..)

import History.Types
import Player.Types
import Wallpaper.Types

type alias Model =
    { history : History.Types.Model
    , player : Player.Types.Model
    , wallpaper : Wallpaper.Types.Model
    }


type Msg
    = HistoryMsg History.Types.Msg
    | PlayerMsg Player.Types.Msg

Notice that Msg is also a parent type that categorizes the child messages. Container init will need to call every child init function, update its state from the results and call the commands they've returned using Cmd.batch:

module Container.State exposing (init, update, subscriptions)

import Container.Types exposing (..)
import History.State
import Player.State
import Wallpaper.State


init : ( Model, Cmd Msg )
init =
    let
        ( history, historyCmd ) =
            History.State.init

        ( player, playerCmd ) =
            Player.State.init

        wallpaper =
            Wallpaper.State.init
    in
        ( Model history player wallpaper
        , Cmd.batch
            [ Cmd.map HistoryMsg historyCmd
            , Cmd.map PlayerMsg playerCmd
            ]
        )

You might have noticed how we map child commands to Container Msg values. This is very important, because it allows us to categorize the messages in the Container update :

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        HistoryMsg fMsg ->
            let
                ( history, historyCmd ) =
                    History.State.update fMsg model.history
            in
                ( { model | history = history }, Cmd.map HistoryMsg historyCmd )

        PlayerMsg fMsg ->
            let
                ( player, playerCmd ) =
                    Player.State.update fMsg model.player
            in
                ( { model | player = player }, Cmd.map PlayerMsg playerCmd )

Subscriptions needs to be mapped called as a batch;

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Sub.map HistoryMsg (History.State.subscriptions model.history)
        , Sub.map PlayerMsg (Player.State.subscriptions model.player)
        ]

Finally we're ready to put the views together. As we categorized the model and the messages properly, it'll be straightforward:

view : Model -> Html Msg
view model =
    div []
        [ History.View.view model.history
            |> Html.map HistoryMsg
        , Player.View.view model.player
            |> Html.map PlayerMsg
        , Wallpaper.View.view model.wallpaper
        ]

See full example in Github.

4. Wrap Up

Hopefully this was useful for you. Open up a pull request for any improvements, corrections. You can also drop me an e-mail for sharing any thoughts or asking questions.

Here are some other reasources that I recommend:

Community channels:

Other:

Back