![]() |
From Elm in Action by Richard Feldman
This article gives a concrete example of the Elm Architecture and how it works that you can follow along with. |
Save 37% on Elm in Action. Just enter code fccfeldman into the discount code box at checkout at manning.com.
How do we keep data flow manageable as our code scales?
JavaScript offers a staggering selection of data flow architectures to choose from, but Elm has only one. It’s called the Elm Architecture, and the Elm Runtime is optimized for applications that follow it. We’ll learn about the Elm Architecture as we add interactivity to an app called Photo Groove.
Figure 1 shows a preview of the architecture we’ll be building toward in this article. Don’t worry if this doesn’t make sense yet! We’ll get there, one step at a time.
Figure 1 The Elm Runtime uses the Elm Architecture to manage data flow
Let’s begin where data flow naturally begins: with application state as a whole.
Representing Application State with a Model
Back in the Wild West days of the Web, it was common to store application state primarily in the DOM itself. Is that menu expanded or collapsed? Check whether one of its DOM nodes has class="expanded"
or class="collapsed"
. Need to know what value a user has selected in a dropdown? Query it out of the DOM at the last possible instant.
This approach turned out to scale poorly, particularly as applications grew more complex and unit testing became increasingly important. By using benchmark testing it is possible to test the characteristics of the application. Today it’s common practice to store application state completely outside the DOM, and to propagate changes from that independent state over to the DOM as necessary. This is what we do in the Elm Architecture.
Declaring a Model
We’re going to store our application state separately from the DOM, and refer to that state as our model.
Definition A model stores the current state of an application. Any value necessary to render a user interface should be stored in its model.
Listing 1 Adding a Model
initialModel =
[ { url = "1.jpeg" } ?
, { url = "2.jpeg" } ?
, { url = "3.jpeg" } ?
]
main =
view initialModel ?
? We’ll add more fields beyond url later
? Pass our new initialModel record to view
Excellent! Now we have an initial model to work with. It contains a list of photos, each of which is represented by a record containing a url
string.
Writing a view function
Next we’ll render a thumbnail for each photo in our list. A typical Elm application does this through a view function, which describes how the DOM should look based on its arguments.
At the top of a typical Elm application is a single view function, which accepts our current model as an argument and then returns some Html
. The Elm Runtime takes the Html
returned by this view function and renders it.
This will be easier to do if we first write a separate viewThumbnail
function, which renders a single thumbnail as Html
. We can set the stage for that design by with the following view
implementation:
Listing 2 Splitting out viewThumbnail
urlPrefix = ?
"http://elm-in-action.com/" ?
view model =
div [ class "content" ]
[ h1 [] [ text "Photo Groove" ]
, div [ id "thumbnails" ] []
]
viewThumbnail thumbnail =
img [ src (urlPrefix ++ thumbnail.url) ] [] ?
? We’ll prepend this to strings like “1.jpeg”
? Prepend urlPrefix to get a complete URL like “http://elm-in-action.com/1.jpeg”
Figure 2 illustrates how our current model
and view
connect to the Elm Runtime.
Figure 2 Model and view connecting with the Elm Runtime
Next, we’ll iterate over our list of photo records and call viewThumbnail
on each one, in order to translate it from a dusty old record to a vibrant and inspiring img
.
Fortunately, the List.map
function does this.
List.map
List.map
is another higher-order function similar to the List.filter
function. You pass List.map
a translation function and a list, and it runs that translation function on each value in the list. Once that’s done, List.map
returns a new list containing the translated values.
Take a look at Figure 3 to see List.map
do its thing for viewThumbnail
.
Figure 3 Using List.map to transform photo records into img nodes
Because div
is a plain Elm function that accepts two lists as arguments—a list of attributes, followed by a list of child nodes—we can swap out our entire hardcoded list of child img
nodes with a single call to List.map
! Let’s go ahead and do that now.
view model =
div [ class "content" ]
[ h1 [] [ text "Photo Groove" ]
, div [ id "thumbnails" ] (List.map viewThumbnail model)
]
viewThumbnail thumbnail =
img [ src (urlPrefix ++ thumbnail.url) ] []
If you run elm-make PhotoGroove.elm --output elm.js
again to recompile this code, you should see the same result as before. The difference is that now we have a more flexible internal representation, allowing us to add interactivity in a way that was impossible before we connected model
and view
.
Expanding the Model
Now let’s add a feature: when the user clicks on a thumbnail, it’ll become selected—indicated by a blue border surrounding it—and we’ll display a larger version of it beside the thumbnails.
To do this, we first need to store which thumbnail is selected. That means we’ll want to convert our model from a list to a record, to store both the list of photos and the current selectedUrl
value at the same time.
Listing 3 Converting the model to a record
initialModel =
{ photos =
[ { url = "1.jpeg" }
, { url = "2.jpeg" }
, { url = "3.jpeg" }
]
, selectedUrl = "1.jpeg" ?
}
? Select the first photo by default
Next let’s update viewThumbnail
to display the blue border for the selected thumbnail.
That’s easier said than done. Being a lowly helper function, viewThumbnail
has no way to access the model—so it can’t know the current value of selectedUrl
. But without knowing which thumbnail is selected, how can it possibly know whether to return a selected or unselected img
?
It can’t! We’ll have to pass that information along from view
to viewThumbnail
.
Let’s rectify this situation by passing selectedUrl
into viewThumbnail
as an additional argument. Armed with that knowledge, it can situationally return an img
with the "selected"
class—which our CSS has already styled to display with a blue border—if the url
of the given thumbnail
matches selectedUrl
.
viewThumbnail selectedUrl thumbnail =
if selectedUrl == thumbnail.url then
img
[ src (urlPrefix ++ thumbnail.url)
, class "selected"
]
[]
else
img
[ src (urlPrefix ++ thumbnail.url) ]
[]
Comparing our then
and else
cases, we see quite a bit of code duplication. The only thing different about them is whether class "selected"
is present. Can we trim down this code?
Absolutely! We can use the Html.classList
function. It builds a class
attribute using a list of tuples, with each tuple containing first the desired class name, and second a boolean for whether to include the class included in the final class string.
Let’s refactor our above code to the following, which accomplishes the same thing:
viewThumbnail
selectedUrl
thumbnail =img
[ src (urlPrefix ++ thumbnail.url)
, classList [ ( "selected",
selectedUrl
== thumbnail.url ) ]]
[]
Now all that remains is to pass in selectedUrl
, which we can do with an anonymous function. While we’re at it, let’s add another img
to display a larger version of the selected photo.
Listing 4 Rendering Selected Thumbnail via anonymous function
view model =
div [ class "content" ]
[ h1 [] [ text "Photo Groove" ]
, div [ id "thumbnails" ]
(List.map (\photo -> viewThumbnail model.selectedUrl photo)
model.photos
)
, img ?
[ class "large"
, src (urlPrefix ++ "large/" ++ model.selectedUrl)
]
[]
]
? Display a larger version of the selected photo
If you recompile with the same elm-make
command as before, the result should now look like Figure 4.
Figure 4 Rendering the selected thumbnail alongside a larger version.
Looking good!
Replacing Anonymous Functions with Partial Application
Although the way we’ve written this works, it’s not quite idiomatic Elm code. The idiomatic style would be to remove the anonymous function:
Before: List.map (\photo -> viewThumbnail model.selectedUrl photo) model.photos
After: List.map (viewThumbnail model.selectedUrl) model.photos
Whoa! Does the revised version still work? Do these two lines somehow do the same thing?
It totally does, and they totally do! This is because calling viewThumbnail
without passing all of its arguments is an example of partially applying a function.
Definition Partially applying a function means providing one or more of its arguments, and getting back a new function which accepts the remaining arguments and finishes the job.
When we called viewThumbnail model.selectedUrl photo
, we provided viewThumbnail
with both arguments needed to return Html
. If we call it without that second photo
argument, what we get back isn’t Html
, but rather a function—specifically a function that accepts the missing photo
argument and then returns some Html
.
Let’s think about how this would look in JavaScript, where functions aren’t set up to support partial application by default. If we’d written viewThumbnail
in JavaScript, and wanted it to support partial application, it would look like this:
function viewThumbnail(selectedUrl) {
return function(thumbnail) {
if (selectedUrl=== thumbnail.url) {
// Render a selected thumbnail here
} else {
// Render a non-selected thumbnail here
}
};
}
Functions that can be partially applied, such as the one in this JavaScript code, are known as curried functions.
Definition A curried function is a function that can be partially applied.
All Elm functions are curried. That’s why when we call (viewThumbnail model.selectedUrl)
we end up partially applying viewThumbnail
, but not getting an undefined
argument or an error.
In contrast, JavaScript functions aren’t curried by default. They are, instead, tupled –which means they expect a complete “tuple” of arguments. (In this case, “tuple” refers to “a fixed-length sequence of elements,” not specifically one of Elm’s Tuples.)
Elm and JavaScript both support either curried or tupled functions. The difference is which they choose as the default:
- In JavaScript, functions are tupled by default. If you’d like them to support partial application, you can first curry them by hand—like we did in our JavaScript
viewThumbnail
implementation above. - In Elm, functions are curried by default. If you’d like to partially apply them…go right ahead! They’re already set up for it. If you’d like a tupled function, write a curried function that accepts a single Tuple as its argument, then destructure that tuple.
Table 1 shows how to define and use both curried and tupled functions in either language.
Table 1 Curried functions and Tupled functions in Elm and JavaScript
Elm | JavaScript | |
Curried Function |
|
|
Tupled Function |
|
|
Total Application |
|
|
Total Application |
|
|
Partial Application |
|
|
We can use our newfound powers of partial application to make view
more concise! We now know we can replace our anonymous function with a partial application of viewThumbnail
.
Before: List.map (\photo -> viewThumbnail model.selectedUrl photo) model.photos
After: List.map (viewThumbnail model.selectedUrl) model.photos
TIP In Elm, an anonymous function like (\
foo -> bar baz
foo)
can always be rewritten as (bar baz)
by itself. Keep an eye out for this pattern; it comes up surprisingly often.
Here’s how our updated view
function should look.
Listing 5 Rendering Selected Thumbnail via partial application
view model =
div [ class "content" ]
[ h1 [] [ text "Photo Groove" ]
, div [ id "thumbnails" ]
(List.map (viewThumbnail model.selectedUrl) model.photos) ?
, img
[ class "large"
, src (urlPrefix ++ "large/" ++ model.selectedUrl)
]
[]
]
? Partially apply viewThumbnail with model.selectedUrl
Because all Elm functions are curried, it’s common to give a helper function more information by adding an argument to the front of its arguments list.
For example, when viewThumbnail
needed access to selectedUrl
, we made this change:
Before: List.map viewThumbnail model.photos
After: List.map (viewThumbnail model.selectedUrl) model.photos
Because we added the new selectedUrl
argument to the front, we could add it using partial application instead of an anonymous function. This is a common technique in Elm code!
TIP Because operators are functions, you can partially apply them too! List.map ((*) 2) [ 1, 2, 3 ]
evaluates to [ 2, 4, 6 ]
.
Incidentally, currying is named after acclaimed logician Haskell Brooks Curry. The Haskell programming language is also named after his first name, and whether the Brooks Brothers clothing company is named after his middle name is left as an exercise to the reader.
Handling Events with Messages and Updates
Now that we can properly render which thumbnail is selected, we need to change the appropriate part of the model whenever the user clicks a different thumbnail.
If we were writing JavaScript, we might implement this logic by attaching an event listener to each thumbnail:
thumbnail.addEventListener(“click”, function() { model.selectedUrl = url; });
Elm wires up event handlers a bit differently. Similarly to how we wrote a view
function that used Virtual DOM nodes to describe our desired page structure, we’re now going to write an update
function that uses messages to describe our desired model.
Definition A message is a value used to pass information from one part of the system to another.
When the user clicks a thumbnail, a message will be sent to an update
function as follows:
Figure 5 Handling the event when a user clicks a thumbnail
The format of our message is entirely up to us. We could represent it as a string, or a list, or a number, or anything else we please. Here’s a message implemented as a record:
{ operation = "SELECT_PHOTO", data = "2.jpeg" }
This record is a message which conveys the following information:
“We should update our model to set 2.jpeg
as the selectedUrl
.”
The update
function receives this message and does the following:
- Looks at the message it received.
- Looks at our current model.
- Uses these two values to determine a new model, then returns it.
We can implement our “select photo” logic by adding this update
function right above main
:
update msg model =
if msg.operation == "SELECT_PHOTO" then
{ model | selectedUrl = msg.data }
else
model
Notice how, if we receive an unrecognized message, we return the original model unchanged. This is important! Whatever else happens, the update
function must always return a new model, even if it happens to be the same as the old model.
Adding onClick to viewThumbnail
We can request that a SELECT_PHOTO
message be sent to update
whenever the user clicks a thumbnail, by adding an onClick
attribute to viewThumbnail
:
viewThumbnail selectedUrl thumbnail =
img
[ src (urlPrefix ++ thumbnail.url)
, classList [ ( "selected", selectedUrl == thumbnail.url ) ]
, onClick { operation = "SELECT_PHOTO", data = thumbnail.url }
]
[]
The Elm Runtime takes care of managing event listeners behind the scenes, so this one-line addition is the only change we need to make to our view. We’re ready to see this in action!
The Model-View-Update Loop
To wire our Elm application together, we’re going to change main = view model
to the following, which incorporates update
according to how we’ve set things up.
main =
Html.beginnerProgram
{ model = initialModel
, view = view
, update = update
}
The Html.beginnerProgram
function takes a record with three fields:
model
– A value that can be anything.view
– A function that takes a model and returns aHtml
node.update
– A function that takes a message and a model, and returns a new model.
It uses these arguments to return a description of a program, which the Elm Runtime sets in motion when the application starts up. Before we got beginnerProgram
involved, main
could only render static views. beginnerProgram
lets us specify how we want to react to user input!
Figure 6 demonstrates how data flows through our revised application.
Figure 6 Data flowing from the start of the program through the Model-View-Update loop
Notice that view
builds fresh Html
values after every update
. That might sound like a lot of performance overhead, but in practice, it’s almost always a performance benefit!
This is because Elm doesn’t recreate the entire DOM structure of the page every time. Instead, it compares the Html
it got this time to the Html
it got last time and updates only the parts of the page that are different between the two requested representations.
This approach to “Virtual DOM” rendering, popularized by the JavaScript library React, has several benefits over manually altering individual parts of the DOM:
- Updates are automatically batched to avoid expensive repaints and layout reflows
- It becomes far less likely that application state will get out of sync with the page
- Replaying application state changes effectively replays user interface changes
Because onClick
lives in the Html.Events
module, we’ll need to import
it:
import Html.Events exposing (onClick)
And with that final touch…it’s alive! You’ve now written an interactive Elm application!
The complete PhotoGroove.elm
file should look like this:
Listing 6 PhotoGroove.elm with complete Model-View-Update in place
module PhotoGroove exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
urlPrefix =
"http://elm-in-action.com/"
view model =
div [ class "content" ]
[ h1 [] [ text "Photo Groove" ]
, div [ id "thumbnails" ]
(List.map (viewThumbnail model.selectedUrl) model.photos)
, img
[ class "large"
, src (urlPrefix ++ "large/" ++ model.selectedUrl)
]
[]
]
viewThumbnail selectedUrl thumbnail =
img
[ src (urlPrefix ++ thumbnail.url)
, classList [ ( "selected", selectedUrl == thumbnail.url ) ]
, onClick { operation = "SELECT_PHOTO", data = thumbnail.url }
]
[]
initialModel =
{ photos =
[ { url = "1.jpeg" }
, { url = "2.jpeg" }
, { url = "3.jpeg" }
]
, selectedUrl = "1.jpeg"
}
update msg model =
if msg.operation == "SELECT_PHOTO" then
{ model | selectedUrl = msg.data }
else
model
main =
Html.beginnerProgram
{ model = initialModel
, view = view
, update = update
}
Let’s compile it once more with elm-make PhotoGroove.elm --output elm.js
. If you open index.html
, you should be able to click a thumbnail to select it. Huzzah!
Figure 7 Our final Photo Groove application
Figure 8 shows where things ended up.
Figure 8 Our final Elm Architecture setup
Congratulations on a job well done!
For more information on Elm, check out the book on liveBook here and see this Slideshare presentation.