One of the reasons why Elm is so reliable is because we can write our application logic using 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 also perform operations that cause side effects, such as sending an HTTP request to a remote server or saving data to a local storage.
Earlier in the Pure Functions section, we learned what side effects are in the context of a function. In this section, we’ll learn more about how they affect an entire application.
One of the fundamental rules all pure functions must adhere to is that an expression always evaluates to the same result in any context. This means we are guaranteed to receive the same output for an input no matter how many times we apply it. 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 quicker. 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 were expecting, we could be certain that it is behaving as expected. We wouldn’t need to worry about creating an elaborate scaffold of mock objects just so we could verify that the function under test sent proper messages to its collaborators.
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.
“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.
It’s crucial to understand that we don’t have to worry about rendering the HTML code or figuring 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. Tell the 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.
2. Get notified when something happens.
Here are some examples:
- Listen for web socket messages.
- Listen for location changes.
- Listen for clock ticks.
Everything is Data
As mentioned earlier in the Model View Update - Part 1 section, the
beginnerProgram function from the
Html module is responsible for wiring our initial model, view, and update functions together.
We can think of the
beginnerProgram 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 our
update functions don’t talk to each other directly or to the outside world. Elm runtime handles all interactions between these functions and the outside world. Because of this, these functions can be pure. Although the
initialModel doesn’t take any inputs, it will always return the same record.
view function will also return the same HTML output given the same model. The
update function might look a little tricky.
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 the commands and tasks which we will cover later in this chapter are also data.
Program as 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 write our programs. 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 another function to transform it to yet another form. So on and so forth until we have the final output which we present to the user.
This approach to building programs drastically reduces the overall complexity in an application. 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 almost 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 the commands in the next section.