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) — 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.
-- MODEL
type alias Model = { count: Int }
initModel = { count = 0 }
-- UPDATE
type Msg = Reset | Increment
update : Msg -> Model -> Model
update msg model =
case msg of
Reset ->
initModel
Increment ->
{ count = 1 + model.count }
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
.
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:
Html.Events
), andToday, 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 Msg
s:
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.
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.
html
package, browser
package, Debug
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.