Functional Programming
A functional programming language hides the von Neumann architecture of the computer. Functional programming is not writing commands to update memory cells. Rather, it is declaring axioms, which are statements that are true by definition rather than proof, and relationships between data. These axioms and relationships do not change over time; they are universal and eternal truths.
This view of programming is a significant departure from the view we adopt when writing C++ and Java. In imperative languages, variables may vary. In Haskell, they do not. We don't set i
to 0. We recognize that i
is 0 and will always be 0. That means we can't write a for loop that changes i
, or any kind of loop for that matter, because loops require mutability. This probably sounds like extreme asceticism, but Haskell provides alternative mechanisms for iterating that do not compromise immutability.
In truth, Haskell does support mutability and imperative programming. However, mutability in Haskell is an advanced topic that we will not discuss. Learning to operate under immutability is enough of a goal.
If mutation is outlawed, either by the language or by our choice, we cannot write a subprogram that produces a side effect. For example, in Java, we can write this method to prepend an element at the front of a list:
void prepend(int newItem, ArrayList<Integer> items) {
items.add(0, newItem);
}
void prepend(int newItem, ArrayList<Integer> items) { items.add(0, newItem); }
This method doesn't return anything. Instead, its work of mutating the list is a side effect. Side effects are a hallmark of imperative programming and the von Neumann architecture.
There are no void functions in functional programming—no functions that produce side effects. To write prepend
in a functional style, we have the function construct and return a brand new list instead of mutating the original:
FixedList<Integer> prepend(int newItem, FixedList<Integer> items) {
return new FixedList(newItem, items);
}
FixedList<Integer> prepend(int newItem, FixedList<Integer> items) { return new FixedList(newItem, items); }
We could write this version of the function in Haskell since it doesn't mutate the list. Note that FixedList
class is only hypothetical; it does not exist in the Java standard library.
Why is immutability something to strive for? Consider a television tuned in to the big game. A mutable television is inherently unsafe. If we step out of the room momentarily, someone may change the channel out from under us. We have three options for keeping the television tuned to the game:
- Live alone and be the only one with access to the television. That also means we're responsible for all the cooking and cleaning, which consumes our time. Likewise, restricting ourselves to a single thread of execution slows down a computation.
- Put a lock on the television and have only a single key. Changing the channel becomes an ordeal, as the key must be found.
- Make the television immutable. Once the television exists, it is stuck to that channel forever. If someone wants to watch a different channel, they get a new TV.
None of these solutions is perfect. They represent the tradeoffs of programming. Immutability means we create new data instead of modifying existing data, but it also means that the data can be shared between functions and threads without having to synchronize access or make independent copies.
Even if we don't adopt Haskell as one of our pet languages, we still benefit from applying the functional style that we learn by writing Haskell. When we remove side effects, even in languages that permit them, our code becomes less dependent on volatile state and easier to test.
A function in an imperative language is a sequence of statements. Each statement mutates the bank of memory cells in order to set the stage for the next statement. Since mutation cannot happen in Haskell, functions are not a series of statements. Rather they are a single expression. The value of that single expression is the function's return value. Because every chunk of code yields a value, sometimes Haskell and other functional languages are described as expression-oriented languages.
Java programs are built out of objects passing messages to one another. Haskell programs are built of functions calling each other. Functions in Haskell are first-class citizens that can be passed around and returned as easily as conventional data. They can also be chained together to form complex calculations. Given their importance, Haskell provides several ways to define functions, which we will examine in a later chapter. For now, we focus on the expressions that form the body of a function.