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.

A close-up of a race track with an Elm logo overlaid

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.