Introduction Into Elm (Pt.2)
Part 2: The Elm Architecture (TEA)
“Introduction into Elm” is aimed at developers who might have a JavaScript background or a very specific React background or simply want to get started with a functional language on the front-end. If you have been playing around with and/or have been thinking about getting started with Elm, this writeup is intended to help you gain a deeper understanding of the language.
At the end of this course you should have a fundamental understanding of what Elm is, how to build applications with Elm and be ready to tackle more advanced topics like building your own modules or packages. So why even try to write an introduction to Elm then? Aren’t there enough resources out there that cover the basics? Maybe, but there can never be enough resources when a language hasn’t gained mainstream appeal yet and furthermore it’s a great way to reflect on one’s own understanding of the topic.
We will gradually move from covering the very basics to building a full functioning application. I plan to cover this in a multi-part series. Every part being a small chapter, where we tackle a specific topic.
Part 2, “The Elm Architecture (TEA)”, will mainly focus on building our first small examples and gradually start to cover more advanced topics explaining the Elm Architecture (TEA) along the way.
In case you have missed out on part 1 or want to recap on the very basics checkout “Part 1: Getting Started For The Absolute Beginner” first. Just to quickly reiterate on the first installment, we went through how to set up Elm, render a basic string to the screen, learned know how to work with the REPL and should have a basic grip on the most fundamental data structures.
Basic Examples
To set things off, let’s write the classic Counter example. To follow along you can either run Elm locally (also see the Basic Example section in Part1 for a detailed walkthrough on how to install Elm) or use the online editor available at http://elm-lang.org/try (recommended for starters) or the more advanced editor at https://ellie-app.com.
Expected outcome: understanding Html.beginnerProgram, Messages, type alias definitions and model, update and view in TEA.
Let’s begin with writing a new file called Counter.elm.
module Counter exposing (..)import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
Let’s define a module Counter and import Html as well as Html.Events and expose onClick. Taking a quick a look at the html package, we notice that the package includes Html, Html.Attributes, Html.Events and other modules. Html.Events offers a number of mouse as well input helpers. We’re interested in capturing any click events in this specific case, that’s why we imported onClick.
Every module in Elm has a main function, so let’s define our main function by using the beginnerProgram function in the Html module.
main =
Html.beginnerProgram
{ model = model
, update = update
, view = view
}
We will get into more detail about what model, update and view are, but let’s define our model first.
type alias Model = Int
In our specific Counter example we’re interested in the actual count which is in an integer. So the type alias for our Model is Int. This looks trivial at first glance, but imagine we’re dealing with larger structures. Let’s take the following example, where we’re dealing with todo items and have a function toggleTodo. How would the type annotation for toggleTodo look like?
toggleTodo : { id: Int, text: String, completed: Boolean }
-> { id: Int, text: String, completed: Boolean }
This is hard to read already, now imagine we have a record with even more fields. Now what if we would define a type alias Todo?
type alias Todo = { id: Int, text: String, completed: Boolean }
By defining the type alias, we can suddenly rewrite the type annotation to:
toggleTodo : Todo -> Todo
So back to our Counter example. Now that we have defined a type alias, let’s also define our model.
model : Model
model = 0
We will see more advanced example regarding model, but for now we should have a basic understanding of how to define the model.
Next, we’ll need to take care of defining how we react to changes in our App. For that, we will need to define Messages and an update function that declares what happens when we receive one of the defined messages.
type Msg
= Inc
| Dec
So we have defined a type Msg using a union type. Union types are useful when we need to describe complex data. You will very often come across union types in Elm. Just take a look at the following short example.
type DataSet e a
= NotStarted
| Fetching
| Failure e
| Success a
We can express a common state inside our application, where we might not have started to fetch yet or might be currently loading the data. This enables us to write a function that expects a DataSet and knows how to handle all specific cases. So suddenly we can write a view function that expects a type DataSet like this:
dataSetView : DataSet -> Html Msg
dataSetView dataSet =
case dataSet of
NotStarted ->
... Fetching ->
...
We will come across union types again, when we start building our application.
But what are Messages? These are things that can take place in our application. In this specific case we know that a user might want to increment or decrement the counter. So by declaring our type Msg, we list all the possible messages that we want to respond to. This is also where the update function comes into play, as we have to specify what happens when we receive any of the messages.
update : Msg -> Model -> Model
update msg model =
case msg of
Inc ->
model + 1
Dec ->
model - 1
As you can see, we defined what happens when we receive an Inc and a Dec. If you know redux, this might remind you of a reducer, we can clearly see the influence of the Elm architecture on redux here. Another interesting fact is that we have to handle all possible messages else the Elm compiler will complain. This is another strength that Elm has to offer, you can’t leave out any messages, everything has to be handled appropriately.
So we have our model and our update by now, but we’re still missing one important piece of the puzzle, the view.
view : Model -> Html Msg
view model =
div []
[ button [ onClick Inc] [ text "Inc" ]
, text (toString model)
, button [ onClick Dec ] [ text "Dec" ]
]
There is not really too much to say about our view. We always receive the model and define what should get rendered to the screen. It might be interesting to note that everything is a function here. Our div is a function that expects a list of attributes and a list of child elements, the text function expects a string and so on. So we have built a Counter example and can verify that clicking on the Inc button really increments our model and clicking on the Dec button decrements it.
Checkout out the complete example.
Now that we have a better understanding of how an Elm application is structured, let’s build on the previous example and see how we can subscribe to actions happening outside of our application.
More Basic Examples
Our next example will listen on key presses to highlight a row. By default we will highlight the very first row.
Expected outcome: understanding Html.program, subscription and how to dynamically render a List of Html elements.
First off all, we will need to create a new file, let’s call it HighlightRow.elm.
Let’s begin by importing everything we need. By now we should know how to define as well as import a module.
module HighlightRow exposing (main)import Html exposing (Html, div, text)
import Html.Attributes exposing (style)
import Keyboard exposing (presses, KeyCode)
We’re importing our well known Html module which exposes div and text functions as well as Html.Attributes, where we will use the style function to style our rows later on. Finally we also need to listen to key presses and get a hold of the keyCode, this is why we need to import Keyboard. So before we continue let’s install Keyboard.
elm-package install elm-lang/keyboard
elm-package will ask if it should add the dependency to our elm-package.json and if we approve of the upgrade plan.
To install elm-lang/keyboard I would like to add the following
dependency to elm-package.json:"elm-lang/keyboard": "1.0.1 <= v < 2.0.0"May I add that to elm-package.json for you? [Y/n] YSome new packages are needed. Here is the upgrade plan.Install:
elm-lang/dom 1.1.1
elm-lang/keyboard 1.0.1Do you approve of this plan? [Y/n] Y
elm-package takes care of installing the dependency and updating the elm-package.json.
We’re ready to write our main function.
main =
Html.program
{ init = init
, subscriptions = subscriptions
, update = update
, view = view
}
You will notice that we’re using program not beginnerProgram this time. One difference is that we need to also define subscriptions as well as an init function as opposed to model in our previous example. We will get into more detail in a minute, but before that, let’s think about our model.
type alias Row =
{ position : Int
, text : String
}type alias Rows = List Rowtype alias Model =
{ currentIndex : Int
, rows : Rows
}
If we think it through, we will need a list of rows and a current index. We could have taken a shortcut and simply defined our model as:
type alias Model =
{ currentIndex : Int
, rows : List { position : Int, text : String }
}
But at first glance we can see that by clearly specifying what a Row looks like, we are able to explain nicely how our data structures looks like and we can leverage the fact by defining type annotations like the following f.e.:
viewRow : Row -> Html Msg
Our init function should define, as the name implies, our initial model.
init : (Model, Cmd Msg)
init =
(Model 0 [ (Row 0 "Foo"), (Row 1 "Bar"), (Row 2 "Baz") ], Cmd.none)
There is something interesting here, compared to our previous examples. We are returning a (Model, Command)
tuple not simply a Model
. It’s also important to note, that we’re not using any commands in this example, but we will see in depth why they are useful in part 3. For now we don’t really care about any commands and always return Cmd.none
.
Initially we’re saying that our index is at position zero and that we have 3 rows to begin with.
[ (Row 0 "Foo"), (Row 1 "Bar"), (Row 2 "Baz") ]
A quick recap: another benefit of defining a type alias Row, is that now we can call Row with an integer and a string, which we are effectively doing in our init function when defining the initial rows.
Next, let’s take care of our update function, but first let’s make sure we have a type Msg in place.
type Msg = KeyPress Int
Here, we’re simply stating that any KeyPress messages should be handled via our update function and further that we will also receive a keyCode of type Int.
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
KeyPress code ->
let
boundary = List.length model.rows - 1
result =
case code of
97 ->
if model.currentIndex < boundary then
model.currentIndex + 1 else 0 115 ->
if model.currentIndex > 0 then
model.currentIndex - 1 else boundary _ ->
model.currentIndex
in
({ model | currentIndex = result }, Cmd.none)
This looks like a lot is happening here. Let’s break this down and go through the above code step by step.
update msg model =
case msg of
KeyPress code ->
This part should be clear by now, we’re covering any KeyPress messages with a keyCode payload.
let
boundary = List.length model.rows - 1
result =
case code of
97 ->
if model.currentIndex < boundary then
model.currentIndex + 1 else 0 115 ->
if model.currentIndex > 0 then
model.currentIndex - 1 else boundary _ ->
model.currentIndex
in
What we see here is a let expression, which is useful when we need to declare a variable like result, in this specific case. Again, we handle any possible case code might have and assign a value to result. the _ ->
means handle any other case, due to the fact that we need to make sure that any other keyCode aside from 97 (a) and 115 (s) is also considered otherwise our example will not compile.
...
in
({ model | currentIndex = result }, Cmd.none)
Finally we’re assigning the result to our currentIndex and returning the updated model. Again, we’re returning a (Model, Command)
tuple here, just like in our init function.
Our view code is composed of a viewRows and a viewRow function which take care of displaying the rows and highlighting the currently selected row.
view : Model -> Html Msg
view model =
div []
[ text
("Selected Row Index is " ++ (toString model.currentIndex))
, viewRows model
]
Our main view function calls viewRows with the model.
viewRows : Model -> Html Msg
viewRows model =
let
getRow = viewRow model.currentIndex
in
div [ style [("width", "800px")] ]
(List.map getRow model.rows)
Here, we’re iterating over our rows and calling the getRow function, which in itself is the partially applied viewRow. Again, we’re using let ... in
to define the getRow. Creating the rows is done by mapping via List.map and calling getRow for every single item.
viewRow : Int -> Row -> Html Msg
viewRow selectedIndex row =
let
basicStyle = [("width", "200px")]
getStyle index =
if (row.position == selectedIndex) then
List.append basicStyle [ ("color", "red") ]
else
basicStyle
in
div [ style (getStyle row.position) ]
[ text row.text ]
Our viewRow finally checks if the row’s position is equal to the selectedIndex and if so, adds a style definition (“color”, “red”)
to the basic styling. We can also see how basic styling can be applied in Elm. The style function expects a List (String, String)
containing the property and value. This approach should be only used lightly. There are more efficient ways to style an Application in Elm as we will see in a later part of the tutorial.
All that is left to do, is to define our subscription function.
subscriptions : Model -> Sub Msg
subscriptions model =
(presses KeyPress)
With subscriptions we have the capability to listen to inputs that are outside our application, f.e. mouse events or key presses. To get our function to work we will need to subscribe to all key presses via presses.
There are situations where might need to listen to more than one input.
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ presses KeyPress
, Mouse.moves MouseMsg
]
By using Sub.batch we can pass in a list of subscriptions and get a subscription in return that takes care of listing to all the provided subscriptions.
Checkout the complete example.
Summary and Outlook
The second introductory part was mainly focused on introducing the Elm Architecture (TEA) and should have covered the basics. You should be able to write small modules, understand what subscriptions are and how to dynamically render a list of items. We still need to cover more basics before we can start building our real world example. Part 3 will focus on side-effects handling via Commands, JavaScript interop and more fundamental topics.
Part 1: Part 1: Getting Started For The Absolute Beginner
Part 3: Effects, JavaScript Intertop and more fundamentals.
Incase you have any questions, please leave feedback on Twitter.