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.
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.)
-- 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
.
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"
},
...
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.
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.