The way that most of us sell functional programming is all wrong. The first thing we say about it is that the style lacks mutable state (although most functional languages allow it). So we’re selling it by talking about what it doesn’t have. Unfortunately, it’s not until late in a programmer’s career (and some never get there) that he realizes that less power in a language is often better, because “freedom-from” can be more important than “freedom-to” in systems that will require maintenance. This makes for an ineffective sales strategy, because the people we need to reach are going to see the statelessness of the functional style as a deficit. Besides, we need to be honest here: sometimes (but not often) mutable state is the right tool for the job.
Often, functional programming is sold with side-by-side code snippets, the imperative example being about 30 lines long and relatively inelegant, while the functional example requires only six. The intent is to show that functional programming is superior, but the problem is that people are likely to prefer whatever is most familiar to them. A person who is used to the imperative style is more likely to prefer the imperative code, because they are more used to it. What good is a reduction to six lines if they seem impenetrable? Besides, in the real world, we use mutable state all the time: for performance and because the world actually is stateful. It’s just that we’re mindful enough to manage it well.
So what is it that makes functional programming superior? Or what is it that makes imperative code so terrible? The issue isn’t that state is inherently evil, because it’s not. Small programs are often easier to read in an imperative style than a functional one, just as goto improves the control flow of many small procedures. The problem, rather, is that stateful programs evolve in bad ways when programs get large.
A referentially transparent function is one that returns the same output per input. This, and immutable data, are the atoms of functional programming. Such functions actually have precise (and usually, obviously intended) semantics which means that it’s clear what is and what is not a bug, and unit testing is relatively straightforward. Contrast this with typical object-oriented software where an object’s semantics are the code, and it’s easy to appreciate why the functional approach is better. There’s something else that’s nice about referentially transparent functions, which is that they can’t be changed (in a meaningful way) without altering their interfaces.
Object-oriented software development is contrived to make sense to non-technical businessmen, and the vague squishiness associated with “objectness” appeals to that crowd. Functional programming is how mathematicians would prefer to program, and programming is math. When you actually care about correctness, the mathematician’s insistence on integrity even in the smallest details is preferable.
A functional program computes something, and intermediate computations are returned from one function and passed directly into another as a parameter. Behaviors that aren’t reflected in a returned value might matter for performance but not semantics. An imperative or object-oriented program does things, but the intermediate results are thrown away. What this means is that an unbounded amount of intermediate stuff can be shoved into an imperative program, with no change to its interface. Or, to put it another way, with a referentially transparent function, the interface-level activity is all one needs to know about its behavior. On the other hand, this means that for a referentially transparent function to become complex will require an equally complex interface. Sometimes the simplicity provided by an imperative function’s interface is desirable.
When is imperative programming superior? First, when a piece of code is small and guaranteed to stay small, the additional plumbing involved in deciding what-goes-where that functional programming can make the program harder to write. A common functional pattern when state is needed is to “thread” it through referentially transparent functions by documenting state effects (that may be executed later) in the interface, which can complicate interfaces. It’s more intuitive to do a series of actions than build up a list of actions (the functional style) that is then executed. What if that list is very large? How will this affect performance? That said, my experience is that the crossover point at which functional programming becomes strictly preferable is low: about 100 lines of code at most, and often below 50. The second case of imperative programming’s superiority is when the imperative style is needed for performance reasons, or when an imperative language with manual memory management (such as C or C++) is strictly required for the problem being solved. Sometimes all of those intermediate results must be thrown away because there isn’t space to hold them. Mutable state is an important tool, but it should almost always be construed as a performance-oriented optimization.
The truth is that good programmers mix the styles quite a bit. We program imperatively when needed, and functionally when possible.
Imperative and object-oriented programming are different styles, and the latter is, in my view, more dangerous. Few programmers write imperative code anymore. C is imperative, Java is full-blown object-oriented, and C++ is between the two depending on who wrote the style guide. The problem with imperative code, in most business environments, is that it’s slow to write and not very dense. Extremely high-quality imperative code can be written, but it takes a very long time to do so. Companies writing mission-critical systems can afford it, but most IT managers prefer the fast-and-loose, sloppy vagueness of modern OOP. Functional and object-oriented programming both improve on the imperative style in terms of the ability to write code fast (thanks to abstraction) but object-oriented code is more manager-friendly, favoring factories over monads.
Object-oriented programming’s claiming virtue is the ability to encapsulate complexity behind a simpler interface, usually with configurability of the internal complexity as an advanced feature. Some systems reach a state where that is necessary. One example is the SQL database, where the typical user specifies what data she wants but not how to get it. In fact, although relational databases are often thought of in opposition to “object-oriented” style, this is a prime example of an “object-oriented” win. Alan Kay’s original vision with object-oriented programming was not at all a bad one: when you require complexity, encapsulate it behind a simpler interface so that (a) the product is easier to use, and (b) internals can be improved without disruption at the user level. He was not saying, “go out and write a bunch of highly complex objects.” Enterprise Java is not what he had in mind.
So what about code rot? Why is the functional style more robust against software entropy than object-oriented or imperative code? The answer is inherent in functional programming’s visible (and sometimes irritating) limitation: you can’t add direct state effects, but have to change interfaces. What this means is that adding complexity to a function expands its interface, and it quickly reaches a point where it’s visibly ugly. What happens then? A nice thing about functional programming is that programs are build up using function composition, which means that large functions can easily be broken up into smaller ones. It’s rare, in a language like Ocaml or Clojure when the code is written by a competent practitioner, to see functions longer than 25 lines long. People break functions up (encouraging code reuse) when they get to that point. That’s a really great thing! Complexity sprawl still happens, because that’s how business environments are, but it’s horizontal. As functional programs grow in size, there are simply more functions. This can be ugly (often, in a finished product, half of the functions are unused and can be discarded) but it’s better than the alternative, which is the vertical complexity sprawl that can happen within “God methods”, and in which it’s unclear what is essential and what can be discarded.
With imperative code, the additional complexity is shoved into a series of steps. For-loops get bigger, and branching gets deeper. At some point, procedures reach several hundred lines in length, often with the components being written by different authors and conceptual integrity being lost. Much worse is object-oriented code, with its non-local inheritance behaviors (note: inheritance is the 21st-century goto) and native confusion of data types and namespaces. Here, the total complexity of an object can reach tens of thousands of lines, at which points spaghettification has occurred.
Functional programming is like the sonnet. There’s nothing inherently superior about iambic pentameter and rhyming requirements, but the form tends to elevate one’s use of language. People find a poetic capability that would be much harder (for a non-poet) to find in free verse. Why? Because constraint– the right kind of constraint– breeds creativity. Functional programming is the same way. No, there’s nothing innately superior about it, but the style forces people to deal with the complexity they generate by forcing it to live at the interface level. You can’t, as easily, throw a dead rat in the code to satisfy some dipshit requirement and then forget about it. You have to think about what you’re doing, because it will effect interfaces and likely be visible to other people. The result of this is that the long-term destruction of code integrity that happens in most business environments is a lot less likely to occur.