Understanding the Elm Architecture
Elm is a pure, functional language with managed effects. This means that none of your Elm code will ever directly cause effects. Here are some useful effects:
- Updating your application's state
- Rendering and updating the DOM
- Making HTTP calls
- Getting the current time (not technically an effect, but still impure)
As you can imagine, the vast majority of programs need to cause effects to be considered useful. Elm lets you tell it to cause effects for you: You give it a description of the effects you'd like to cause, and Elm does it for you. A clear separation exists: Your code, the "inside world", and everything else, the "outside world".
The Elm runtime is your link to the outside world: It is responsible for running your code, and it is the only place where data crosses the boundary between the inside and outside worlds. Apart from this link to the runtime, your code is completely isolated.
The runtime starts your program by calling a single function called main
. In a browser-based GUI application,
this function will call one of several possible functions to initialize it. The one I will use as an example
is Html.program
, as it embodies
the "Elm Architecture" well without getting too complex:
program : { init : (model, Cmd msg)
, update : msg -> model -> (model, Cmd msg)
, subscriptions : model -> Sub msg
, view : model -> Html msg }
-> Program Never model msg
Specialized to the actual types most programs use by convention:
program : { init : (Model, Cmd Msg)
, update : Msg -> Model -> (Model, Cmd Msg)
, subscriptions : Model -> Sub Msg
, view : Model -> Html Msg }
-> Program Never Model Msg
You define 4 lifecycle functions and give them to Html.program
as a record, and it will call these functions
at various points in your program's lifetime
Application state (the model)
Your code does not explicitly maintain any state; the inside world is stateless. The runtime is responsible for maintaining state in the outside world, where this is allowed.
The state is contained in a single data structure, usually with the type Model
:
type TodoItem = {
title : String
, done : Bool
}
type alias Model = List TodoItem
When your program starts, the runtime will call init
which returns the initial state:
init : (Model, Cmd Msg)
init = ([TodoItem "Procrastinate" True, TodoItem "Finish writing this article" False, TodoItem "Profit" False], Cmd.none)
init
returns a tuple of (Model, Cmd Msg)
. Ignore the Cmd Msg
for now. The Model
is returned by init
to the runtime, which saves this as the initial state of the application.
After initialization, the runtime listens for events in the outside world. Your program defines a list of events that can happen:
type Msg
= NewItem Item
| RemoveItem Item
| CheckItem Item
| UncheckItem Item
When one of these events happen, the runtime will call your update
function and pass it, along with the current
state:
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
NewItem item ->
(item :: items, Cmd.none)
RemoveItem item ->
(List.filter (\i -> i == item) items, Cmd.none)
CheckItem item ->
(List.map (\i -> if i == item then {i | done = True} else i) items, Cmd.none)
UncheckItem item ->
(List.map (\i -> if i == item then {i | done = False} else i) items, Cmd.none)
Typically, update
will pattern match on the event, the Msg
, to see which type of event it is and then do something
different for each event. Of course, no change happens inside update
as it runs in the inside world where mutation
is forbidden. It will return a new value as part of the tuple (Model, Cmd Msg)
. The runtime will receive this new
Model
across the boundary and store it as the new state in the outside world.
You can imagine the runtime working something like this:
// Initialize the state by calling `init`
var state = YourApp.init()[0];
onEvent(function(event){
// When an event happens, run `update` and save the new state
state = YourApp.update(event, state)[0];
});
The runtime is basically an event loop, and your update
function is the callback.
Rendering to the DOM (the view)
It is not possible to interact with the DOM from the inside world. None of your Elm code can see the DOM, as it exists only in the outside world. As with your application state, it is the runtime's responsibility to maintain the state of the DOM according to your instructions.
After it calls init
, and after each call to update
, the runtime will call your view
function, passing it the
new state that was returned by your function. view
will then return a description of how the DOM should look in the
form of an Html Msg
. You may be familiar with
this "virtual DOM" concept from other tools.
view : Model -> Html Msg
view model =
todoItems model
todoItems : List TodoItem -> Html Msg
todoItems items =
ul [] (List.map todoItem items)
todoItem : TodoItem -> Html Msg
todoItem item =
li [classList [("checked", item.done)]] [text item.title]
An Html Msg
represents a DOM node with attributes and zero or more child nodes. In the example, the list of
TodoItem
s is turned into an Html Msg
tree representing this HTML:
<ul>
<li class="checked">Procrastinate</li>
<li>Finish writing this article</li>
<li>Profit</li>
</ul>
The runtime will receive this description across the boundary and apply it to the actual DOM in the outside world. This makes sure the state of the DOM always reflects the current state of the application.
The runtime now looks more like this:
// The root node of our application's view
var root = document.body;
// Initialize the state by calling `init`
var state = YourApp.init()[0];
// Render the initial state to the DOM
renderDOM(root, YourApp.view(state));
onEvent(function(event){
// When an event happens, run `update` and save the new state
state = YourApp.update(event, state)[0];
// Call `view` with the new state and apply the result to the DOM
renderDOM(root, YourApp.view(state));
});
Where do events come from?
They don't come from nowhere. Each Msg
that is received by your update
ultimately originates from something
you did. There are 3 sources of events:
- DOM events
- Commands
- Subscriptions
DOM events
In the node tree returned by your view
function, you may include descriptions of DOM event handlers. When the
runtime sees these it will attach the necessary handlers to the DOM.
todoItem : TodoItem -> Html Msg
todoItem item =
li [classList [("checked", item.done)], onClick (CheckItem item)] [text item.title]
The example has been changed to add an onClick
handler to the virtual DOM node. When this is returned from
view
, the runtime will apply it to the DOM:
<ul>
<li onClick="onEvent(CheckItem(state[0]))" class="checked">Procrastinate</li>
<li onClick="onEvent(CheckItem(state[1]))">Finish writing this article</li>
<li onClick="onEvent(CheckItem(state[2]))">Profit</li>
</ul>
The Msg
part of Html Msg
means that events of type Msg
(and only this type) can originate from this node.
When a DOM event occurs, the runtime will translate this to one of the events you've listed as part of the Msg
type and call your update
and view
functions.
Commands
Commands are structures of the type Cmd Msg
that instruct the runtime to perform effects. A good example is making an HTTP call:
type Msg =
...
| TodoItemsResponse (Result Http.Error (List TodoItem))
fetchTodoItems : Cmd Msg
fetchTodoItems =
Http.get "/todo-items.json" todoItemsDecoder
|> Http.send TodoItemsResponse
Note the new event type that's been added to Msg
and the use of TodoItemsResponse
as an argument to
Http.send
. The resulting Cmd
will
include information on how to construct a Msg
that will be passed to your update
function. It is essentially
a callback: You are only describing an effect that will happen at some point in the future, and when it happens
the runtime will notice this and call your update
with the result:
update msg model =
case msg of
...
TodoItemsResponse result ->
case result of
Err error ->
-- Ignore the error and just return the same model
(model, Cmd.none)
Ok items ->
-- Update the model to be the new list of TodoItems
(items, Cmd.none)
Html.program
, as the mediator between the inside and the outside world, specifies 2 functions that may return a
Cmd
:
program : { init : (Model, Cmd Msg)
, update : Msg -> Model -> (Model, Cmd Msg)
, subscriptions : Model -> Sub Msg
, view : Model -> Html Msg }
-> Program Never Model Msg
Both init
and update
return a tuple of (Model, Cmd Msg)
. Most of the time, you will simply return Cmd.none
,
which means "do nothing". But if you do want to cause an effect, this is where you must return the Cmd
representing your effect. You can create as many Cmd
s as you want in the inside world, but nothing will happen
until you hand them off to the runtime by returning them from one of these functions.
The following init
sets the initial state to be an empty list and returns a Cmd
which
instructs the runtime to try and fetch the actual TodoItem
list from the server:
init : (Model, Cmd Msg)
init = ([], fetchTodoItems)
When the server responds, the runtime will call update
with the TodoItemsResponse
which will update the model
if the response was successful.
The pretend-runtime updated to take commands into account:
// The root node of our application's view
var root = document.body;
// Initialize the state by calling `init`
var [state, cmd] = YourApp.init();
// If a command was returned, execute it asynchronously
if (cmd) executeCmd(cmd, function(res){ onEvent(cmd.createMsg(res)); });
// Render the initial state to the DOM
renderDOM(root, YourApp.view(state));
onEvent(function(event){
// When an event happens, run `update` and save the new state
[state, cmd] = YourApp.update(event, state);
// If a command was returned, execute it asynchronously
if (cmd) executeCmd(cmd, function(res){ onEvent(cmd.createMsg(res)); });
// Call `view` with the new state and apply the result to the DOM
renderDOM(root, YourApp.view(state));
});
Subscriptions
Subscriptions are for repeating events. Some examples are WebSocket connections that represent a stream of messages, or a "tick" event that occurs every second to update the inside world's time.
Subscriptions are represented by the Sub Msg
type, and there is only 1 way to tell the runtime about your interest in them: The subscriptions
function executed
by Html.program
, which takes a Model
and returns a Sub Msg
.
A simple example is Time.every
, which
returns a Sub Msg
that results in an event containing the current time every second:
type Msg =
...
| Tick Time
update msg model =
case msg of
Tick time ->
-- Do something with `time` here
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every Time.second Tick
The subscriptions
function is called whenever the Model
changes. If you're not an experienced functional
programmer, this may seem weird and even dangerous to you. But as you know, this function runs in the inside
world where everything is safe and pure: All it does is return a description of the repeating events you're
interested in. The runtime will check if that description has changed since the last time and reconcile its
internal state in the outside world if necessary.