In this article series, we’ll look into learning Elm quickly.

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

Note: Examples in this article use Elm 0.19.

Let’s build an app

In this article, we will discuss immutable data structures and the basics of the elm architecture. To make things more practical, we will build a very simple app.

Immutable data structures

Elm is a functional programming language. One of the tenets of functional programming languages is that data structures are immutable. Once created, they cannot be changed. In practice, this means that functions in Elm will take in a data structure as its argument, and then return an entirely new data structure.

If you think about it, it makes perfect sense. If all the data structures are immutable, how do we deal with change? More specifically, how do we deal with changes that Elm functions introduce on the existing data? The only obvious answer is to create entirely new data.

How does Elm apply this in practice?

Let’s say that we want to build an app in the health niche. The app is a simple countdown app that displays only a button and a number. The app starts with the number 5. The idea of the app is for a user to press a button whenever they have eaten a healthy snack, say, a piece of fruit. That way, a user will improve their healthy habits by making sure that they have eaten five pieces of fruit a day.

The Elm architecture

The very minimum that we need to understand about the Elm architecture is that it is made up of four things:

  • Model,
  • View,
  • Message, and
  • Update

It is important to note that the architecture is usually described as being made up of only three things: Model, View, and Update.

However, for the sake of clarity and easier learning, at this point of our journey into Elm, we can think of the Message as an equal building block of the Elm architecture.

Thus, for the sake of having a clear mental model of all the things that are in play here, we will think of the Elm architecture as having four constituent pieces, rather than three.

The Model

The Model holds our app state.

In our Fruit Counter app, the Model holds, as its one and only data structure, an integer.

When we first run our app, the Model holds the value of 5.

Since it is represented in terms of data structures, and since data structures in Elm are immutable, our model will have to be updated as a copy of the previous data plus changes made to it.

The model is updated whenever a function operates on it.

When will a function operate on the model?

Since we are building a very simple app, the only time a function will operate on the model is when the user clicks the one button in our app. Clicking this one button will decrement the current value in the model by 1. Since data in Elm is immutable, a function will have to return a new copy of the model, with updates taken into account.

The View

The View can be thought of as the way to see the Model on our screen.

The View is a function, and we pass the Model to it.

The Model is a parameter of the View function.

Thus, the View takes in the Model as its parameter, and returns HTML, which will be rendered in the browser.

Another way to think of the View is this: The View is a way to allow the user to interact with the Model.

The View is sort of like a visual API for the Model. It is a way for the user to manipulate the Model in a structured way.

When a user interacts with the View, they manipulate the Model by changing its state. Since our app is very simple, the only way to change the state is to press that one button in our app, which will decrement the current value that our model holds.

This one button press will make the View send a Message to the Update.

The Message

After the user has interacted with the View (pressed the button), the notification of this action will be sent to the Update function in the form of a Message. Since there is only one possible action that can happen, that Message is simply: decrement.

Of course, in any realistic app, the Message will have to have a lot more logic, but for the sake of simplicity, let’s leave it at decrement.

The Update

The Update function receives the Message.

Next, the Update function determines how to update the state of our app, that is, how to update the Model, based on the Message received. Once the update is made, a new Model is created and it renders the View. The user then interacts with the View which results in the Message being sent again. The Update receives the Message and updates the Model, and the cycle continues.

Unidirectional data flows

An interesting way of looking at our app is as flows of data. The reason to have this concept and use it in our discussions is because it is a helpful way of looking at the changing of state in our Elm app.

Since Elm is built on a lot of restrictions, it only makes sense to have this idea of restrictions applied to its architecture as well.

If you look at what is happening in our simple app, you will notice that the data always moves in only one direction: From Model to View to Message to Update to Model. That’s what unidirectional data flow basically is. Uni-directional data flow in Elm

This approach allows us to be able to track state changes with ease, and it also makes it a lot less difficult to reason about these changes.

Building the Fruit Counter app

Let’s begin with our app skeleton in Main.elm:

module Main exposing (main)

import HTML exposing (HTML, text)


main : HTML msg
main =
    text "Hello, World!"

Let’s look at what the above code does.

Exposing everything we need

We’ll update module Main by adding two dots inside parentheses, which will expose everything in this Main module.

We will do the same for the imported Html module, since we want do be able to use all the available HTML functions. Specifically, we need access to the h1, p, and button functions.

Next, the line that reads main : HTML msg is an optional type annotation, so to demonstrate that we can work without it, we’ll comment it out by placing two hyphens and a space at the beginning of this line.

Including type annotations is considered a best practice, and we have commented it out only to show that our app will still run without a type annotation (as well as to demonstrate how to add one-line comments in Elm).

Our app now looks like this:

module Main exposing (..)

import HTML exposing (..)


-- main : HTML msg
main =
    text "Hello, World!"

To preview the app at this stage, simply compile it with elm make src/Main.elm.

The Model

Let’s add our model:

-- MODEL

type alias Model = 
    Int

Our Model is just a simple integer type.

For now, we’ll ignore what this type alias signifies, as it will only distract us from understanding fundamental concepts at the moment. We will return to types later on in the book.

The View

Our View function will receive the current model and will return the following HTML:

-- VIEW

view model =
    div [] [ h1 [] [ text ("Fruit to eat: " ++ (toString model)) ] ] 

Note that the view function takes in the model parameter. In Elm, parameters are listed after the function name, separated from it by the space character.

Let’s now examine what HTML will be rendered from the code above.

The code that we are assigning to the view function is located to the right of the equals sign (Elm’s assignment operator). In this code to the right of the assignment operator, we are running the div function.

Like all other HTML functions in Elm, the div function has two pairs of square brackets. The first pair of square brackets optionally lists the HTML attributes for the div function, and the second one lists the content of the actual div.

We have left the first pair of square brackets empty, which means we have not given any HTML attributes to our div function. Then, inside the second pair of square brackets of the div function, we have passed in the h1 function.

Like all other HTML functions, the h1 function also has two pairs of square brackets. In the preceding example, our h1 function has no attributes specified (as the first pair of brackets is empty - just like we did with the div, its parent function/element). Inside the second pair of brackets of the h1 function, we call the text function.

The text function will render a text node. The text to be output inside the text node is surrounded by parentheses. Inside the parentheses, we are using a string literal and we are concatenating to it the value of the model, converted to a string (using the toString function).

We’ve just learned about one of Elm’s operators, the ++ operator. In Elm, ++ is the string concatenation operator, used to join together two separate strings.

Since we will initialize our model with the value of 5, the preceding code for the view function will initially return the following HTML code:

<div>
    <h1>Fruit to eat: 5</h1>
</div>

Our view function is now ready at the most rudimentary level. Next, we’ll deal with the Message section.

The Message

Let’s now look at the Message section, where we’ll declare a new type, and we’ll call it Msg:

-- MESSAGE

type Msg = 
    Decrement

As explained earlier in this article, we will not deal with explaining types at this time.

The Update

It’s time to add our update. In the previous step, we have declared our special type of Msg. We’ll pass it to the update function:

-- UPDATE

update msg model =
    model - 1

By now, it should be easy for you to guess what the update function will do: it will receive a msg and a model, and it will return a copy of the model, decremented by 1.

All we have to do now is make the Model, View, and Update work together, and for that, we’ll use the Browser.sandbox function.

Adding the Browser.sandbox function

It’s time to add the Browser.sandbox function, which we will assign to the main function.

Our main function currently looks like this:

-- main : HTML msg
main =
    text "Hello, World!"

Let’s add our updated main function, which will now have the Browser.sandbox function assigned to it:

-- main : HTML msg
main =
  Browser.sandbox { model = 5, view = view, update = update }

As you can see, we just call the Browser.sandbox function. Then, we pass it the model, view, and update functions, and assign each of them a value.

We initialize the model with the value of 5. For the update, we assign it to the value of the update function. The view is similarly set to the value of the view function.

After we made these changes to our main function, the full code of our app now looks like this:

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text, h1)
import Html.Events exposing (onClick)


-- MODEL
type alias Model = 
    Int

-- INITIAL MODEL
initialModel =
    5

-- VIEW
view model =
    div [] [ h1 [] [ text ("Fruit to eat: " ++ (String.fromInt model)) ] ]   

-- MESSAGE
type Msg = 
    Decrement

-- UPDATE
update msg model =
    model - 1

-- main : HTML msg
main =
  Browser.sandbox { init = initialModel, view = view, update = update }

If we ran our app at this point, everything would work, and we would get the following output on our screen: Fruit to eat: 5.

Even though our app is really basic, it’s great that everything works and that we are not getting any compiler errors at this time. However, there is one thing we did not do, and that is that we have not added the button, the one entry point to initiate the changing of our app’s current state.

Before we do add this button, feel free to look at the preceding code and think a bit about a perfectly stateless app. Currently, our app’s model will never change, as the update section of our code will never be run.

Let’s rectify that by adding a button.

Views, buttons, and events

Let’s begin by simply adding a static button to our app. We’ll do that by updating the view function with the following code:

-- VIEW
view model =
    div [] 
        [ h1 [] [ text ("Fruit to eat: " ++ (String.fromInt model)) ]
        , button [ onClick Decrement ] [ text "Eat fruit" ]
        ]   

Now let’s save all our changes and compiling our Elm app again.

Next, we’ll open the compiled index.html in the browser, and we’ll see this: Fruit to eat app now works

Whenever you click the Eat fruit button, the decrement message will be sent to the update function, the update function will receive both the decrement message and the current immutable model and return a completely new model - 1, and the cycle will continue as explained earlier.

Running our app now gives us a simple, rudimentary, but working app built with Elm!

When you click the Eat fruit button, the view function hits the first pair of square brackets on the button function, and it watches for the click event. We have provided the onClick function with the Msg to be sent once the button is clicked. Since there is only one possible message in our app, once the click event is triggered, the view function will send the Decrement message to the update function.

Once the update function has received the message, it will return a new model, and the new model gets rendered by the view function.

However, there is an issue. If we keep clicking the button, our app will ultimately move into counting negative numbers, and that’s just impossible. A user should not have negative 2 fruits left to eat.

Let’s fix this issue next.

Constraining the Decrement message

In order to constrain the Decrement message, let’s begin by looking at the current update function:

-- UPDATE
update msg model =
    model - 1

Now, let’s introduce an if-else expression to cater for different possible scenarios:

-- UPDATE
update msg model =
    if model > 0 then model - 1 else model == 5

Unfortunately, the preceding code does not produce the desired results. Instead, we get this compiler message:

TYPE MISMATCH - The 2nd branch of this `if` does not match all the previous branches:

29|     if model > 0 then model - 1 else model == 5
                                         #^^^^^^^^^^#
The 2nd branch is:

    #Bool#

But all the previous branches result in:

    #number#

#Hint#: All branches in an `if` must produce the same type of values. This way, no
matter which branch we take, the result is always a consistent shape. Read
<https://elm-lang.org/0.19.0/union-types> to learn how to “mix” types.

#Hint#: Only #Int# and #Float# values work as numbers.

The preceding issue is a great example of Elm constraints in practice. Because of the way Elm is set up, each branch must return the same data type. In our case, we can either return Booleans for either branch of the if-else expression, or return numbers, but we can not have them mixed up.

So, how do we rectify this? To keep things simple and still use an if-else expression we started with, let’s think of a terse way to avoid type mismatch. Our counter value should never go under zero, so we can simply do this:

-- UPDATE
update msg model =
    if model > 0 then model - 1 else model + 5

Using the preceding code, our if-else logic will always return a number. The update function’s message value will be incremented by 1 as long as it’s greater than zero. Otherwise (in case it is zero), it will be increased by 5.

Let’s look at the full completed app as it is now:

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" ]
        ]
    
-- MESSAGE
type Msg =
    Decrement
    
-- UPDATE
update msg model =
    if model > 0 then model - 1 else model + 5

-- main : Html msg
main = Browser.sandbox 
    { init = 5
    , update = update
    , view = view 
    }

Sometimes you might see initialModel referenced as the value of init passed to Browser.sandbox.

Here’s the code of our app using that approach:

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, text, h1)
import Html.Events exposing (onClick)


-- MODEL
type alias Model = 
    Int

initialModel =
    5

-- VIEW
view model =
    div [] 
        [ h1 [] [ text ("Fruit to eat: " ++ (String.fromInt model)) ]
        , button [ onClick Decrement ] [ text "Eat fruit" ]
        ]

-- MESSAGE
type Msg = 
    Decrement


-- UPDATE
update msg model =
    if model > 0 then model - 1 else model + 5

-- main : HTML msg
main =
  Browser.sandbox { init = initialModel, view = view, update = update }

Regardless of which way you choose for your starter app, the resulting behavior is exactly the same.

In the next article, we’ll build a simple FizzBuzz app in Elm.