Having covered what functional programming is, I wanted to spend a minute or two discussing why I want to learn functional programming in the first place. I’m sure we have all heard vague things about “side-effects”, “immutability”, and “composition”, but I wanted to dive a bit deeper on the topic to describe what — to me — is important about functional programming.
The key differentiating feature of (pure) functional programs is that they provide referential transparency. An expression is said to be referentially transparent if it can be replaced with its corresponding value without changing the program’s behaviour.
An interesting effect of referential transparency is that it makes your code context-independent, meaning that an expression can be run in any order or in any context and it will always return the same result. This feature allows you to safely substitute a pure function with a different implementation depending on the context. A pure function can also be easily optimized because you describe what you want without prescribing how to do it. The compiler or runtime is free to choose how to execute or rearrange statements, including making some code parallel if possible.
Another benefit of referential transparency is that it eliminates side-effects from your code. Referential transparency requires that functions be free of any code that can modify the program state outside of the function. If you write a function that has side-effects and then substitute the entire function with its return value, any side-effects from executing the function would be lost — a function with side-effects breaks referential transparency and you lose the ability to use that function in any context.
A follow-on effect of referential transparency is immutability. Any piece of code that mutates variables, objects, or data structures outside of a function are not referentially transparent — they cannot be cleanly substituted for the function’s value while keeping the program’s behaviour the same. Referentially transparent functions are, by their nature, support immutable data structures.
Why Functional Programming Matters
Referential transparency (or some variation of it) is often cited as the primary advantage of functional programming. Yet, according to John Hughes in Why Functional Programming Matters:
Such a catalogue of “advantages” is all very well, but one must not be surprised if outsiders don’t take it too seriously. It says a lot about what functional programming isn’t (it has no assignment, no side effects, no flow of control) but not much about what it is. The functional programmer sounds rather like a medieval monk, denying himself the pleasures of life in the hope that it will make him virtuous. To those more interested in material benefits, these “advantages” are totally unconvincing.
At the time of this paper’s publication, structured programming was being taken to the somewhat logical extreme of object-oriented programming as a means of coping with the increased complexity inherent in larger programs, leading to the development of languages like SmallTalk and Eiffel which emphasized using purely object-oriented structures to focus on software quality. The success of these languages led to the introduction of object-oriented concepts to existing languages like C (C++ and Objective-C), and the creation of new languages like Java (started in 1991). Since that time, Java has gone on to become the most widely used languages in the world, with an estimated 9 million developers.
In the midst of the fever surrounding object-oriented and structured programming, Hughes paper provides an argument on why functional programming principles still matter, and why we shouldn’t be so quick to ignore the advantages functional programming offers while adopting object-oriented paradigms. In his analysis, Hughes first tries to go beyond the typical list of arguments for functional programming that reference immutability and side-effects, to get to the core point that makes functional programming important — modularity.
Our ability to decompose a problem into parts depends directly on our ability to glue solutions together. … Functional programs provide two new kinds of glue — higher-order functions and lazy evaluation.
Support for higher-order functions enables functions to be passed as arguments and returned from functions. This feature allows you to compose your program as a series of functions that can be glued together to produce new results. The typical examples of this are map and reduce, that allow you to apply a function to a set of values.
Support for lazy evalution — delaying the evaluation of an expression until its value is needed — makes it possible to glue whole programs together. Lazy evaluation,
makes it practical to modularize a program as a generator that constructs a large number of possible answers, and a selector that chooses the appropriate one. While some other systems allow programs to be run together in this manner, only functional languages (and not even all of them) use lazy evaluation uniformly for every function call, allowing any part of a program to be modularized in this way. Lazy evaluation is perhaps the most powerful tool for modularization in the functional programmer’s repertoire.
Together, higher-order functions and lazy evaluation provide strategies for modularizing your program that are impossible without referential transparency. And this strength, according to Hughes, is why functional programming matters.
Simple Made Easy
So, functional programming offers referential transparency, and referential transparency enables powerful modularization techniques like higher-order functions and lazy evaluation. We intuitively know that more modular programs are somehow better, but why exactly? The key to answering this question is to understand simplicity.
Rich Hickey, author of Clojure, sums this up nicely in his talk Simple Made Easy. In this talk, he contrasts the notion of “Easy” with the notion of “Simple”: “Easy” meaning approachable, available, and ready to use, and “Simple” meaning uncomplicated, and free from elaboration. In software, “Easy” implies ready-to-use libraries that do a lot of stuff under the hood, without necessarily making you aware of it. These libraries can be very complex, with a web of dependencies and frameworks, but they make it “Easy” to build a basic application. Ruby on Rails is a great example of “Easy” software that is very complex. “Simple”, on the other hand, implies uncomplicated pieces that are easy to understand, easy to change, easy to debug, and that can be flexibly combined.
Hickey argues that functional programming in the modular style allowed by referential transparency gives you the ability to build simple software that is also easy. Modular programming enables you to partition the elements of your design horizontally (partitioning), and vertically (stratification).
Partitioning and stratification don’t imply simplicity, but are enabled by it. If you make simple components, you can horizontally separate them and you can vertically stratify them.
Referential transparency (and therefore immutability) is the key to making things simple.
Having state in your program is never simple. State is easy, but introduces complexity because it intertwines value and time. State intertwines everything it touches directly or indirectly, and it is not mitigated by modules and encapsulation.
What matters in software is: does the software do what is supposed to do? Is it of high quality? Can we rely on it? Can problems be fixed along the way? Can requirements change over time? Referential transparency that allows you to modularize your program in new ways, and helps answer these questions by making things as simple as possible. And this is why functional programming matters.