In the Immutability section, when we applied the version of the
doubleScores function, we got what we expected.
But when we applied it after
doubleScores, we received a completely different result.
A function that exhibits an inconsistent behavior like this is said to be impure. Whereas in Elm, all functions are pure, which means they will always return the same output given the same input, regardless of the order they’re applied in.
There is one more condition a function has to meet to become pure: it shouldn’t cause any side effects. Before we understand what a side effect is we need to first understand state.
What is a state?
A state represents all the information stored at a given instant in time that a function has access to. In the Immutability section we wrote this code:
The state of
doubleScores is whatever information stored in the parameter
scores and constant
multiplier because it has access to both. If we were to introduce another constant called
differentMultiplier like this:
That would also become a part of the
doubleScores function’s state. Even if
doubleScores doesn’t use
differentMultiplier in its definition, it can still access that constant.
A function has a side effect if it modifies its state. In other words, a function causes a side effect if it performs any other action apart from calculating its return value. Elm functions don’t have side effects. The
doubleScores function above cannot modify any part of its state. Because
differentMultiplier are all immutable, Elm prohibits
doubleScores from modifying them. Therefore, an Elm function doesn’t remember one or more preceding events in a given sequence of operations. Each interaction with a function happens based entirely on the information given to it.
In Chapter 5, we’ll learn how the Elm runtime uses various techniques to shield our application code from harmful side effects caused by operations such as sending or reciving data form an HTTP server or listening to web socket messages.
In the Functions section, we learned that a function in mathematics is a relationship from a set of inputs to a set of possible outputs where each input is mapped to exactly one output.
Functions in Elm will always return the same output given the same input and they don’t have any side effects. Their behavior aligns perfectly well with how functions work in mathematics.
- Referential Transparency
- If an expression can be replaced with the value it evaluates to without changing the program’s behavior at all, that expression is said to have referential transparency. Since functions in Elm are expressions that always return the same output given the same input, they are referentially transparent. We can safely replace a function application with its output anywhere in an Elm program, and it will work just the same as it did before.
scores list parameter even though we were very careful not to do that in its implementation.
It can also modify the
multiplier variable which is defined outside of it. Go to
experiment.js and change
multiplier’s value to
3 right above the line where
newScores is returned like this:
Now let’s find out the consequences of changing the state as we’ve done here. Reload
index.html in the browser and enter the following code in the browser console.
So far everything looks good. Let’s see what happens if we apply
doubleScores again to the
It tripled each element in the list which is not what we want. That’s because it reassigned
3 after it was done doubling each element in the previous run. By modifying its state, the
doubleScore function has caused a side effect that resulted in unintended consequences.
Benefits of Pure Functions
What practical benefits do we get from pure functions? They are useful to us in many ways such as:
Easy to understand code - The idea of easy to understand code is very subjective and depends on each programmer’s perception of what easy really means to him or her. That said, when we can rely on our functions to not behave in surprising new ways, understanding our program’s behavior does become somewhat easier in Elm. Functions in Elm aren’t affected by the external state. They are also prohibited from modifying any state. As a result, we don’t need to mentally keep track of things that aren’t in their scope. Just by looking at the input and output we can understand a function’s behavior quite a bit.
Easy to debug - It’s much easier to find the root cause of a bug in a program built by assembling pure functions. Since functions in Elm don’t depend on unrelated code that was executed before them, we can reliably reproduce the bug. Once it’s reproduced, finding the root cause of a bug is also easy. We pause the execution of our program and examine each function’s output. Because a function in Elm depends only on its arguments and other immutable constants in its scope, all the information being fed to a function is right before our eyes. If we see a function producing unexpected output, then we must have incorrectly implemented the logic inside that function. This differs from languages that lack immutability and pure functions. When functions in those languages produce unexpected output, we can’t be certain that the logic is wrong because the function depends on an unpredictable external state.
Easy to test - Verifying that our functions work exactly how we expect them to also becomes easier when they are pure. There are multiple ways to verify a function’s behavior. One such technique is to write unit tests which determine whether an individual unit of source code can be used reliably. In Elm, that individual unit of code is a function. When we write our unit tests, we repeatedly pass values to a function from its input set and verify that the values it produces belong to its output set. Since a pure function doesn’t rely on external states, we don’t need to worry about writing tests that verify whether the function caused any side effects. In contrast, in languages that lack pure functions, writing such tests is not only necessary, but tricky as well. We will learn how to write tests later in this chapter.
Solving complex problems with simple functions - An effective way of solving a problem is to break it into smaller problems first. We can then write small reliable functions that solve each mini-problem separately. We then put the functions back together to create a solution that solves the original complex problem. It’s incredibly difficult to apply this technique if the functions aren’t pure. We will see a concrete example of how to combine pure functions to solve a complex problem later in this chapter.
In the next couple of sections, we’ll dig deeper into how pure functions enable us to create elegant solutions to larger problems through composition and why it’s easier to test them.