Introduction to MVC in Elm

Say that we wanted to write some JavaScript (pseudo)code to keep track of whether the user is currently pressing the mouse button. We might start by defining a mutable variable, and then installing two “callback” functions that get invoked by the JavaScript run-time system when the mousedown and mouseup events are triggered:

// the application "state" or "model"
var isDown = false;

// the "view" of the state
function printIsDown() { mainElement.innerHTML = isDown.toString(); }

// event handlers to update the state
function handleMouseDown() { isDown = true;  printIsDown(); }
function handleMouseUp()   { isDown = false; printIsDown(); }

// the "controller" that maps events to handlers
element.addEventListener("mousedown", handleMouseDown);
element.addEventListener("mouseup",   handleMouseUp);

(Note: The point of this example is to make plain the structure of managing the state, so we will avoid the natural urge to refactor.)

This is quite a roundabout way of implementing what can be described simply as “a boolean that is true only when the mouse button is being pressed.” Furthermore, in a hypothetical typed dialect of JavaScript, the types of these functions would be rather uninformative:

printIsDown : () -> void
handleMouseDown : () -> void
handleMouseUp : () -> void
Element.addEventListener : (string, () -> void) -> void

Matters quickly become more complicated when managing state that depends on multiple events and multiple intermediate computations.

Elm Architecture

The Model-View-Controller (MVC) architecture is a common way to structure systems that need to react in response to events. The MVC paradigm in Elm benefits from the building blocks of typed functional programming.

A program is factored into the following three parts described in The Elm Architecture:

import Html exposing (..)


-- MODEL

type alias Model = { ... }


-- UPDATE

type Msg = Reset | ...

update : Msg -> Model -> Model
update msg model =
  case msg of
    Reset -> ...
    ...


-- VIEW

view : Model -> Html Msg
view model =
  ...


-- MAIN

main : Program Never Model Msg
main =
  Html.beginnerProgram { model = initialModel, view = view, update = update }

initialModel : Model
initialModel = ...

Unlike the manifestation of MVC in the JavaScript example above, where mutable variables and callback functions glue the pieces together, the entire state of the Elm application is factored into a single data structure (Model) and all events are factored into a single data structure (Msg).

The model keeps track of all the information that is needed to produce the desired result. We may be sloppy and sometimes (actually, often) refer to the model as the state, but let us not be tricked into thinking there is anything mutable at the source level. This greatly simplifies the nature of event-based programming.

(We won’t worry about the Never type, which is used internally to guarantee the absence of some failures.)

Example: Count Button Clicks

-- MODEL

type alias Model = { count: Int }

initialModel = { count = 0 }


-- UPDATE

type Msg = Reset | Increment

update : Msg -> Model -> Model
update msg model =
  case msg of
    Reset -> initialModel
    Increment -> { count = 1 + model.count }


The Html, Html.Attribute, and Html.Events libraries provide wrappers around the full HTML5 format. In particular, Html provides

node : String -> List (Attribute msg) -> List (Html msg) -> Html msg

to create an arbitrary kind of DOM node. The library also provides helpers for many common kinds of DOM nodes, such as:

text : String -> Html msg

button : List (Attribute msg) -> List (Html msg) -> Html msg

h1 : List (Attribute msg) -> List (Html msg) -> Html msg

img : List (Attribute msg) -> List (Html msg) -> Html msg

You can peek at the implementation to see how these are phrased in terms of the general-purpose node function.

Likewise, Html.Events provides a general-purpose

on : String -> Decoder msg -> Attribute msg

function and useful helpers for specific events, such as:

onClick : msg -> Attribute msg

A simple user interface with a reset button, increment button, and counter display:

-- VIEW

view : Model -> Html Msg
view model =
  let reset = Html.button [onClick Reset] [Html.text "Reset"] in
  let increment = Html.button [onClick Increment] [Html.text "Increment"] in
  let display = Html.text ("Count: " ++ toString model.count) in
  Html.div [] [reset, increment, display]

Download this program as CountButtonClicks.elm and launch elm-reactor.

Example: Count Mouse Clicks

Let’s create a variation of the counter without buttons, counting mouse clicks instead and using the escape key for reset. For the user interface, we’ll just strip out the buttons:

view : Model -> Html Msg
view model =
  let display = Html.text ("Count: " ++ toString model.count) in
  Html.div [] [display]

The basic Elm architecture has the following structure:

main : Program Never Model Msg
main = 
  Html.beginnerProgram 
    { model = initialModel, view = view, update = update }

initialModel : Model
update : Msg -> Model -> Model
view : Model -> Html Msg

Writing to an HTML window is the primary effect that basic Elm programs can affect.

The “full” Elm architecture has mechanisms for subscribing to additional kinds of incoming events (besides those provided by Html.Events) and issuing commands for outgoing events. For now, we will look only at subscriptions, because mouse and keyboard events in Elm are provided through the subscription mechanism rather than through Html.Events. Later on, we will return to commands, which allow interacting with native code (e.g. JavaScript) and performing effects in addition to computing the Html output to be rendered.

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

init : (Model, Cmd Msg)
update : Msg -> Model -> (Model, Cmd Msg)
view : Model -> Html Msg
subscriptions : Model -> Sub Msg

The view function remains unchanged.

The update function, however, issues a command (of type Cmd Msg defined in Platform.Cmd) in addition to an updated Model. Likewise, the initial program state issues an initial command. In all cases, we use dummy command Cmd.none:

init : (Model, Cmd Msg)
init = (initialModel, Cmd.none)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Reset -> (initialModel, Cmd.none)
    Increment -> ({ count = 1 + model.count }, Cmd.none)

The other change is to define subscriptions which is (effectively) a list of events to subscribe to, and this list can be determined based on the current Model.

Mouse provides the following:

clicks : (Position -> msg) -> Sub msg

Whenever the mouse is clicked at Position position, we want Elm to feed the Increment message into our update function:

subscriptions : Model -> Sub Msg
subscriptions model =
  Mouse.clicks (\position -> Increment)

We subscribe to mouse events no matter what the current Model is and we disregard the click position, so we may choose to remove the unnecessary identifiers:

subscriptions _ =
  Mouse.clicks (always Increment)

Keyboard provides the following:

downs : (KeyCode -> msg) -> Sub msg

Platform.Sub provides

batch : List (Sub msg) -> Sub msg

to define multiple subscriptions.

subscriptions model =
  Sub.batch
    [ Mouse.clicks (always Increment)
    , Keyboard.downs (\keyCode -> ...)
    ]

We want to Reset only when keyCode is escape (ASCII code 27), but we have to return a Msg no matter what. So, we add a new Noop value:

type Msg = Noop | Reset | Increment

update msg model =
  case msg of
    Noop -> (model, Cmd.none)
    ...

Now we are in a position to respond to all clicks and “only” escape downs:

subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.batch
    [ Mouse.clicks (always Increment)
    , Keyboard.downs (\keyCode -> if keyCode == 27 then Reset else Noop)
    ]

As a final touch, let’s use CSS to position the counter display in the center of the window. The use of (<|) below is a way to use a 2-space indent despite the whitespace rules of the language.

view model =
  let style =
    Html.Attributes.style <|
      [ ("position", "fixed")
      , ("top", "50%")
      , ("left", "50%")
      , ("transform", "translate(-50%, -50%)")
      ]
  in
  let display = Html.text ("Count: " ++ toString model.count) in
  Html.div [style] [display]

Download CountMouseClicks.elm and try it out in elm-reactor.

Here’s the compiled version, which requires adding Mouse and Keyboard to elm-package.json:

...
"dependencies": {
    "elm-lang/core": "5.1.1 <= v < 6.0.0",
    "elm-lang/html": "2.0.0 <= v < 3.0.0",
    "elm-lang/mouse": "1.0.1 <= v < 2.0.0",
    "elm-lang/keyboard": "1.0.1 <= v < 2.0.0"
},
...

Compiling to JavaScript

Wait a minute… We are factoring our Elm code into models, views, and controllers just like in the JavaScript code we started with. So, what have we really gained?

Well, in JavaScript, we would write logic in event handlers to determine which parts of the state need to be updated for different events. In Elm, we define a completely Model record for every update, and we leave it to the Elm compiler and run-time to figure out when and how values can be cached and reused. The Elm compiler is left with the responsibility to generate target JavaScript that manages mutable state and event handlers, similar to the pseudocode we started with. So, we have gained a lot!

A naive approach would be to recompute the entire program based on changes to any event. This would, of course, be inefficient and is also unnecessary, because many parts of the computation are likely to be stable across events. Instead, the compiler tracks dependencies in the dependency graph and uses a concurrent message-passing system to more efficiently recompute only those parts of a program that are needed.

We will not go into any of the details of the compilation process, but you can find more information about it in the Reading links posted below. At a basic level, however, our intuitions for how the process might work should resemble our intuitions about how optimizing compilers for functional languages (even without events) work: a source language may be purely functional with immutable data structures being copied all over the place, but we know that, below the hood, the compiler is working to identify opportunities to reuse and cache previously computed results. In fact, we will see much more of this principle in the coming weeks as we study how to realize efficient data structures in purely functional languages.


Reading

Required

Additional

For a more comprehensive background on the implementation of an early version of Elm, you may want to skim parts of Evan Czaplicki’s papers below. Note that features and terminology have evolved since then.