Quickstart Elm 0.19, part 5
Building a bit more advanced Fruit counter app
By: Ajdin Imsirovic 24 October 2019
In this article series, we tried to get into Elm as quickly as possible, by buiding three mini apps up to this point. In this article we’ll shift our focus on some more theoretical concepts, before we move onto the more advanced stuff.
Note: Examples in this article use Elm 0.19.
Revisiting Elm messages
In the second part of this article series we looked at the Elm arhitecture: Model, View, and Update. We also mentioned another important ingredient: Messages, a way for Views to communicate with Updates.
We used a very simple example app, Fruit Counter. The app was simple indeed: the only message our view could ever send to the update function was Decrement
. The simplicity of the app we made was a great way for us to understand the architecture without having to introduce too many concepts that would get in the way of learning.
However, now that we have a rudimentary understanding of all the moving parts and how they fit together, we can talk about another level of complexity related to messages in the Elm arhitecture.
The improved Fruit counter app
In the improved Fruit counter app, we introduce another button, using which we can easily reset the counter to five, for example if we ate only a couple of pieces of fruit before the next day started - so our counter needs to be reset anyway.
Here’s the code:
module Main exposing (..)
import Browser
import Html exposing (..)
import Html.Events exposing (onClick)
-- MODEL
type alias Model =
Int
-- VIEW
view model =
div []
[ h1 [] [ text ("Fruit to eat: " ++ (String.fromInt model)) ]
, button [ onClick Decrement ] [ text "Eat fruit" ]
, button [ onClick Reset ] [ text "Reset counter" ]
]
-- MESSAGE
type Msg =
Decrement | Reset
-- UPDATE
update msg model =
-- if model > 0 then model - 1 else model + 5
case msg of
Decrement ->
if model >= 1 then
model - 1
else
5
Reset ->
5
-- main : Html msg
main = Browser.sandbox
{ init = 5
, update = update
, view = view
}
This app is very similar to our original Fruit Counter, only slightly more advanced. Let’s compare these differences.
After pasting in the code, compile the updated app so that we can start comparing the old and the new version of the app.
In original app, we had the following message:
-- MESSAGE
type Msg =
Decrement
In our updated app, the message is as follows:
type Msg =
Decrement | Reset
In the old app, our message only had the value of Decrement
. It could only be a Decrement
message. In the updated app, we have two options, the Message can be either a Decrement
or and Reset
.
What’s with the type
keyword, and the pipe character, then? It has to do with something known as union types (also known as algebraic data types or tagged unions).
In Elm, a union type is simply a custom type that we can come up with on the fly. In our initial Fruit Counter app, our Msg
union type has only one value: Decrement
. In the updated app, the Msg
union type can have either of the two values: Reset
or Decrement
. To differentiate clearly between possible values in a union type, we use the pipe character.
Let’s make another custom union type in the Elm REPL:
type Furniture = Chair | Table | Sofa
To create a union type, we begin with the type
keyword. Next, we provide the actual type, Furniture
. We created a custom type on the fly, and named it Furniture
! To the right of the assignment operator (the =
sign), we provide the values that the Furniture
union type can have. These values are called type constructors, as you can use them to construct new instances of Furniture
.
Let’s create a new instance of Furniture
in the REPL:
> friendsCouch = Sofa
Sofa : Furniture
We have just constructed a new instance of the Furniture
type. As we can see, the REPL responds with this information — the value is Sofa
, and its type is Furniture
.
Sometimes, it is helpful to explain the same concept in a couple of different ways. Another way of looking at union types is that they are a way for us to describe constructor functions, that is, to define them.
In the update
function, we had:
type Msg = Decrement | Reset
The preceding code means that to make a Msg
, either the Decrement
function or the Reset
function needs to be called.
The theoretical underpinnings of union types are rooted in mathematical logic, namely the set theory, which is basically the study of collections of things. Thus, we can look at a union type as a combination of any number of collections of things. Both union types and the set theory can get quite abstract, but at this point in our learning, suffice it to say that union types are a way to organize messages in Elm apps.
Functions, pattern matching, and case expressions
The goal of this chapter was to build a simple app and learn important theory behind it. We expanded on this goal by comparing our own app with the one from the official docs.
In this section, we will look at the update function of the Buttons App and take it apart in order to have complete understanding of what it does and how it works.
This is important, because once we understand how the update
function works in the Buttons app, we will be able confidently to implement a similar solution and improve our Fruit Counter.
Let’s begin by inspecting the update
function in the newer version of the app:
update msg model =
-- if model > 0 then model - 1 else model + 5
case msg of
Decrement ->
if model >= 1 then
model - 1
else
5
Reset ->
5
We see a new keyword here: case
.
Generally, the way that the case
syntax works is as follows — a variable has a certain value. Based on its value, a certain block of code will execute. When the value is different, the block of code to execute will be different as well. Finally, at the end of a case
expression, there is a block of code that will execute for all the values that were not already specified in the case
expression. In other words, for any unspecified scenario, there is a case
block at the bottom to take care of it. This case
block is called the wildcard and it’s marked with the underscore character, _
. As we can see in the preceding example, there are situations where the wildcard case does not need to be added, because we have already covered all the possibilities.
Elm case
expressions are evaluated via pattern matching, that is, by verifying whether a case
conforms to a pattern. If expressions and case expressions are quite similar. One major difference is that case
expressions match patterns, and if
expressions check for true conditions as ways to determine which code blocks to run.
Looking at the syntax of case
expressions, we can see that they start with the case
keyword, followed by the name of the case expression (in our example, msg
). The name is completely arbitrary; instead of msg
, we could have used anything else. For example:
update anything model =
-- if model > 0 then model - 1 else model + 5
case anything of
Decrement ->
if model >= 1 then
model - 1
else
5
Reset ->
5
As you can see in the preceding code snippet, the first parameter of the update
function and the name of the case expression must be the same. To avoid confusion, it’s best to stick with msg
as the first parameter here, as that is the norm, and you’ll see it used that way in most Elm programs.
So, after the case
keyword, and the name of the case expression, we have another keyword, of
.
Next, we list our cases. The structure of the code is always the same:
Pattern -> Expression to evaluate
If we look at the first case, we can see that it’s written as follows:
Decrement ->
if model >= 1 then
model - 1
else
5
In the preceding code snippet, the pattern to match is Decrement
, and the expression to evaluate is either model - 1
or 5
, based on the input that came in - i.e the value of model
parameter that was passed into the update
function.
The second case, Reset
, is used to set the value of model
to 5
whenever the user clicks the Reset
button.
In the next article, we’ll look into List.map
and List.filter
in Elm.