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) — referred to as Model-Update-View (MUV) in the Elm documentation — 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 parts, described in The Elm Architecture:

import Browser
import Html exposing (..)


-- MAIN

main =
  Browser.sandbox { init = initModel, view = view, update = update }


-- MODEL

type alias Model = { ... }

initModel : Model
initModel = ...


-- UPDATE

type Msg = ...

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


-- VIEW

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

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.

Example 1: Count Button Clicks

Model

-- MODEL

type alias Model = { count: Int }

initModel = { count = 0 }

Update

-- UPDATE

type Msg = Reset | Increment

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

View

The html package provides 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"]
    increment =
      Html.button [onClick Increment] [Html.text "Increment"]
    display =
      Html.text ("Count: " ++ Debug.toString model.count)
  in
    Html.div [] [reset, increment, display]

Download this program as CountButtonClicks.elm and generate this HTML via...

% elm make src/CountButtonClicks.elm --output=CountButtonClicks.html

... or by running elm reactor.

Example 2: 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: " ++ Debug.toString model.count)
  in
    Html.div [] [display]

The "first", simplest version of the Elm architecture that we saw above (via Browser.sandbox) had the following structure:

main : Program () Model Msg
main = 
  Browser.sandbox
    { init = initModel, view = view, update = update }

init : Model

update : Msg -> Model -> Model

view : Model -> Html Msg

Creating an HTML document is the primary effect that basic Elm programs can affect.

The "second" version of Elm architecture (via Browser.element) has mechanisms for:

  • subscribing to additional kinds of incoming events (besides those provided by Html.Events), and
  • issuing commands for outgoing events.

Today, we will look at subscriptions: 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 Flags Model Msg
main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }

init : Flags -> (Model, Cmd Msg)
  --   ^^^^^^^^^^     ^^^^^^^^^^

update : Msg -> Model -> (Model, Cmd Msg)
  --                     ^     ^^^^^^^^^^

view : Model -> Html Msg

subscriptions : Model -> Sub Msg

The type of the view function remains unchanged.

The type of 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 configuration init issues an initial command. In all cases, today we will use the dummy command:

Cmd.none : Cmd msg

Furthermore, the init component is now a function, taking an application-specific Flags argument. Non-trivial Flags are used for more advanced interoperation between Elm and JavaScript. Here, we'll just use the "dummy" unit type.

type alias Flags = ()

init : Flags -> (Model, Cmd Msg)
init () =
  (initModel, Cmd.none)

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

Finally, there is a new component subscriptions which is (effectively) a list of events to subscribe to; this list is defined as a function in terms of the current Model.

Browser.Events provides the following:

onMouseDown : Decoder msg -> Sub msg

Whenever the mouse is clicked, we need to decode the corresponding JavaScript event is and produce an appropriate Msg to feed into our update function. In our application, we don't care about where the mouse was clicked, so we'll ignore the JSON information corresponding to the JavaScript event. As you start building your own HTML programs, see Json.Decode to learn how decoders work. For now, just note that the following decoder ignores the JSON and always returns the specified value:

succeed : a -> Decoder a

Thus, no matter what the current Model is:

subscriptions : Model -> Sub Msg
subscriptions model =
  Browser.Events.onMouseDown (Decode.succeed Increment)

We also want to subscribe to keyboard events. Browser.Events also provides the following:

onKeyDown : Decoder msg -> Sub msg

Platform.Sub provides

batch : List (Sub msg) -> Sub msg

to define multiple subscriptions.

subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.batch
    [ Browser.Events.onMouseDown (Decode.succeed Increment)
    , Browser.Events.onKeyDown (...)
    ]

We want to Reset only when key is "Escape", 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)
    ...

(Are these good message names? I have noopinion.)

Now we are in a position to respond to all clicks and "Escape" downs; we start with a keyDecoder and then Decode.map it to transform it into one that produces Msgs:

subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.batch
    [ Browser.Events.onMouseDown (Decode.succeed Increment)
    , Browser.Events.onKeyDown
        (Decode.map (\key -> if key == "Escape" then Reset else Noop) keyDecoder)
    ]

Again, as you start writing your own HTML programs, read about decoders.

As a final touch, let's use CSS to position the counter display in the center of the window.

view model =
    let
      styles =
        [ ("position", "fixed")
        , ("top", "50%")
        , ("left", "50%")
        , ("transform", "translate(-50%, -50%)")
        ]
      display =
        Html.text ("Count: " ++ Debug.toString model.count)
    in
      Html.div (List.map (\(k, v) -> Attr.style k v) styles) [display]

Download CountMouseClicks.elm and run elm make or launch elm reactor to generate this HTML.

Exercise: Define

uncurry : (a -> b -> c) -> ((a, b) -> c)

and use it in the call to Html.div above.

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 new 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 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

Additional

  • Miscellaneous notes from last year: 2020.0413

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.