Pure Functions
We've learned that in pure functional programming, mutation and side effects are not allowed. Once a variable has been assigned a value, it is stuck with that value forever—to the end of the program. If we can't change variables or produce side effects, how can we perform the following very basic computational tasks?
- incrementing a value
- writing loops that have a counter or iterator that advances through a sequence
- reading input or writing output in ways that rely on a seek pointer
- generating pseudorandom numbers
These are fundamental programming tasks, and we'll need to find a way to achieve them even under the iron fist of immutability.
One consequence of immutability is that functions are entirely predictable. Say we call f(3)
once and get back "jefferson"
. Even if we call f(3)
a thousand times more, we'll get back "jefferson"
every time. There is no modifiable global or static state in the background that may cause the function to return a different value. A function whose behavior is determined entirely from its parameters is a pure function. Developers favor pure functions over impure ones because they are easier to test and prove correct. Haskell's standard library is full of pure functions, and this fact is a major source of its appeal.
The computational tasks listed above all require a memory of what happened in the past so that they can perform an operation in the present. When we call an impure read
function, the I/O library picks up reading from wherever it left off and advances the seek pointer forward to prepare for the next read. When we call an impure random
function, the random number generator derives a new random number from the previous and remembers the new number so a later call can generate the next number.
Recording a memory of the past but also forbidding values from being mutated seem like incompatible goals. But they are not. Haskell minimizes mutation yet allows developers to maintain state. In this chapter we examine how it does so. By its end, you'll be able to answer the following questions:
- How do Haskell developers perform I/O and generate random numbers without violating immutability?
- What syntactic device does Haskell provide so that pure-functional programmers can write programs in an imperative style?
- If Haskell doesn't have loops, how can code be repeated and how can sequences be iterated through?
- If a function always has to produce a value but there is no imperative control flow, how does one deal with bad data and other exceptional situations?
Languages like Ruby, JavaScript, Kotlin, and Scala have co-opted the first-class functions of Haskell and its cousins. But few have gone to the extreme of forbidding mutability, the other major tenet of functional programming. As data gets bigger, programs take longer to run. Programs that allow mutation can't be easily optimized or distributed across hardware. You are therefore likely to see a renewed interest in immutability during your career. Even if you don't use Haskell on a daily basis, it provides a training ground for learning how to employ immutability to your advantage.