5.5

Side Effects

One of the reasons why Elm is so reliable is because we can write our application logic using only pure functions. Pure functions take an input, do some computation, and return an output. That’s their entire job. There is a lot less room for things to go wrong since they don’t perform any other operations that cause side effects, such as sending an HTTP request to a remote server or saving data to a local storage.

Note: Earlier in the Pure Functions section, we learned what side effects are in the context of a function. In this section, we’ll learn how they affect an entire application.

One of the fundamental rules all pure functions must follow is that an expression must always evaluate to the same result in any context. This means we are guaranteed to receive the same output for a given input no matter how many times we apply the function. This allows us to replace all applications of that function with the output it generates throughout our program and achieve the same result.

Wouldn’t our job as programmers be much easier if every line of code we wrote was part of a pure function? Reasoning about what our program does would be much easier. That means we could find the root cause of a bug much more quickly. Testing would also become less painful. We could generate a good sample input set and apply the function under test with values from that set. If the function returned the output we’re expecting, we could be certain that it is behaving as expected.

Real World is Messy

Unfortunately, the real world is messy. If we were to write only pure functions, we would most likely end up with useless programs. They wouldn’t be able to take any input from the user or present an output on a screen or retrieve data from a remote server. Why can’t pure functions do that? Because pure functions guarantee that for a given input it will always return the exact same output. So if we have a function that takes someone’s name as an input and attempts to display it on a screen, can we mark that function as pure? The answer is no. That function cannot guarantee that the name can be displayed on a screen every time we apply it. That’s because we cannot rely on a screen to be working all the time. Perhaps the driver for a graphics card is not working or the graphics memory is full. There could be any number of other reasons that might cause a screen to not work.

The most common way for a program to interact with the outside world is through side effects. The following definition of side effects from Wikipedia explains why they tend to make our application complex and messy.

Definition: “A function or expression is said to have a side effect if it modifies some state or has an observable interaction with calling functions or the outside world. For example, a particular function might modify a global variable or static variable, modify one of its arguments, raise an exception, write data to a display or file, read data, or call other side-effecting functions. In the presence of side effects, a program’s behavior may depend on history; that is, the order of evaluation matters. Understanding and debugging a function with side effects requires knowledge about the context and its possible histories.” - Wikipedia.

Elm Runtime to the Rescue

So how does Elm deal with this conundrum? It employs a rather clever technique to manage side effects originated from interacting with the outside world. To understand this technique, let’s go back to the sign up form example we saw in the Model View Update - Part 2 section. The view function there takes a User model and returns some Elm code representing HTML that is capable of generating messages.

view : User -> Html Msg
view user =
    ...

It’s crucial to understand that we don’t have to worry about rendering the HTML code or figure out how to route the messages originating from the text fields and buttons to our update function. The Elm runtime takes care of that.

One way to look at it is that our code is being placed in a protective cocoon where everything is deterministic. We can always tell what the output will be for a given input. But outside of this cocoon, we can’t tell what output we will get for the same input. The figure below shows how the Elm runtime shields our code from the outside world that is riddled with side effects.

User interface is not the only aspect of outside world that produces side effects. There are others too. Most scenarios in which an Elm app needs to interact with the outside world tend to fall into two categories:

1. Ask Elm runtime to do something. Here are some examples:

  • Send and receive data from a remote HTTP server.
  • Save data to a local storage.
  • Generate random numbers.
  • Request a JavaScript library to perform an operation

2. Get notified when something happens. Here are some examples:

  • Listen for web socket messages.
  • Listen for geolocation changes.
  • Listen for clock ticks.
  • Listen for an output generated by a JavaScript library

Elm offers commands to deal with the scenarios in the first category and subscriptions to deal with the scenarios in the second category.

Everything is Data

As mentioned earlier in the Model View Update - Part 1 section, the sandbox function from the Browser module is responsible for wiring our initial model, view, and update functions together.

main : Program () User Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

We can think of the sandbox function as a proxy to the Elm runtime. Even though it takes care of the wiring, in the end it’s the runtime that renders the HTML and routes messages to proper functions. We already saw how the runtime coordinates the interaction between different functions in our code. Here it is again:

As it turns out the initialModel, view, and update functions don’t talk to each other directly or to the outside world. Elm runtime handles all interactions between them and the outside world. Because of this, those functions can be pure. Although the initialModel doesn’t take any inputs, it will always return the same record.

initialModel : User
initialModel =
    { name = ""
    , email = ""
    , password = ""
    , loggedIn = False
    }

The view function will also return the exact same HTML output given the same model. The update function might look a little tricky.

update : Msg -> User -> User
update message user =
    case message of
        SaveName name ->
            { user | name = name }

        SaveEmail email ->
            { user | email = email }

        SavePassword password ->
            { user | password = password }

        Signup ->
            { user | loggedIn = True }

It takes a message and a model as inputs and returns a new model as an output. The model is just some data. But what about the message? It’s data too. It’s a special type of data that contains information about what action to perform. Similarly commands and subscriptions, which we will cover later, are also data.

Program as a Data Transformation Machine

Now that you have some background, you can understand the clever technique Elm uses to deal with the outside world: it treats everything in our program as data, except the functions that operate on that data. This has profound implications for how we build applications. We can treat our programs as a series of data transformation operations. You take some data (perhaps something user provided), apply a function to transform that data to some other form. Take that and apply a different function to transform it to yet another form. So on and so forth until we have the final output which can be presented back to the user.

This approach to building applications drastically reduces the overall complexity. We will revisit this idea of treating everything as data when we talk about commands and subscriptions later. But for now, hopefully you understand how Elm makes it possible to write all of our logic using pure functions.

The rest of this chapter is dedicated to exploring various techniques the Elm runtime uses to deal with the outside world. We’ll start with commands in the next section.

Back to top
Close