Pure Functions
You've learned that Haskell doesn't allow mutation or side effects. Once a variable has been assigned a value, it is stuck with that value forever—to the end of the program. If you can't change variables or produce side effects, how can you 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 if you feed a function a particular input, it will always yield the same output, no matter how many times you call it. There is no modifiable global or static state in the background that may cause the function to behave differently. 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 you 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 you 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 and forbidding values from being mutated seem like incompatible goals. But they are not. Haskell both forbids mutation and 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 increases in size, mutability becomes a significant concern. Programs that must always check for mutated data can't be easily optimized or distributed across hardware. In these days of plentiful data, you are 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.