Functional programming is a paradigm of programming, a means of structuring and reasoning about code. It is, in essence, about composing functions that transform data. That is, when writing functionally, we write simple functions that transform data in a particular manner. Then we later write some other functions that use our previous functions as building blocks for more complicated transformations. This may not sound all too foreign.
In the object-oriented world, programming is about maintaining state in some controlled fashion. We create object hierarchies to define the world and we operate on some methods of those objects to manipulate the world around us. That is, we compose objects to model and, if we're lucky, we solve problems.
These are both methods of and for abstraction. We write simple components and compose. When a simple component is defined, we can forget its details and begin thinking about bigger components that result from the combination of those smaller ones.
However, there are several problems that creep up on us in the object-oriented and imperative worlds. They are subtle and they rarely, if ever, show themselves directly. These are the problems that are hard to find, hard to debug, and hard to fix. Although, we can see their symptoms.
We notice the symptoms when we attempt to conceptualize or interpret our own code using an ideal or imaginary interpreter. We notice the symptoms when we attempt to test large components. We notice the symptoms when we attempt to split execution paths. Something, somewhere, inevitably fails.
Objects and imperative code are usually, relatively easy to understand on paper. So why is this understanding only on the surface and so easily shattered when we dig further?
Imperative code is certainly testable. We can certainly get to correct solutions. But why is it so difficult to write good, testable code? Why are the correct answers so hidden from view?
Clearly imperative code is composable; yet, why is it often difficult to compose objects and existing functions? What is hindering our ability to do this well?
We can also write concurrent, imperative code. Why, then, is it seemingly so daunting and nearly impossible to get correct?
The lurking monster hiding behind our questions is usually the one thing that makes programming actually useful: side-effects or state. Functions in the imperative world usually encapsulate implicit changes to variables, objects, files, and, in other words, changes to state. These changes usually cost nothing to program and may, in fact, be pinnacle to the function they originate from. How useful would printing text to the console be if you were unable to write a stream of bytes to the character device, that is, your terminal?
It is these hidden, out-of-mind side-effects that can make programming so dangerous to understanding, correctness, and composability, not to mention, concurrency. To overcome this, we can't forget the side-effects lurking in our code. That is, when composing components in the object-oriented world, we still fail to release ourselves from the burden of implementation. We must still page the details of our objects to use them effectively. Our escape is functional programming.
Functional programming allows us to escape from these problems by forcing us to confront the issue of changing states. It gives us guidelines for how to construct our components and not to forget that state changes are inevitable and how to handle them appropriately, without compromising on composition, understandability, and testability. Furthermore, well managed side-effect inducing code, is nearly trivial to make concurrent.
The functions implemented in functional languages must return the same output for the same input. Any dependence of the output on the state, outside of what we give the function, must not change the output of the function. Changes of state are handled in a very controlled manner: they must be marshaled through some channel. Most functional languages handle this in similar ways, but this can also depend on the level of purity of the language. However, this affords us easy-to-understand and easy-to-personally interpret code, and lets us create well defined modularity and testable components. Also, by restricting changes of state (or disallowing them entirely), making code concurrent is essentially free.
Best of all, functional programming isn't something restricted to a particular language; it's a concept that can be followed and used in any language. It's more of a state-of-mind than being partial to a particular set of languages.
Of course, I can't ethically speak about the benefits of functional programming without also warning you of some potential limitations, particularly of the new learning curve and some performance considerations.
Functional programming can feel very limited to newcomers. It's generally difficult to do anything useful in purely functional programming languages, and often, this isn't helped by examples that only show the functional bits in elementary examples.
When transforming data in purely functional languages, we must create new structures for the modified data. If we could otherwise modify the existing data structures, we would be violating our immutability invariant. Thus, when modifying some data structure, we must usually create a new structure. This has fairly obvious performance issues, namely, the memory copies are required to create the new structure on top of performing the actual modifications.
However, there is hope for both of these concerns. Firstly, with respect to performance considerations, functional programs can typically have more sophisticated tooling because it is not only easier for us to understand as mere mortals, but also the tools we write to parse, compile (translate), and execute the functional code written. Some interesting compiler optimizations can be achieved from the shared nothing, process separation, and message passing processing model enforced by the underlying runtime.
That is, functional code, generally, is easier to parse, both by humans and compilers. Therefore, the compiler and runtime can flatten, unroll, and take our functional code and rewrite it into a safe, optimized form. The runtime can also take advantage of object graph information to decrease the number of copies needed to modify data. The graph can later be checked by the runtime and compressed if necessary; this is the basis for garbage collection.
Secondly, with respect to the learning curve, I hope to teach functional programming through more than elementary or intermediate examples by the conclusion of this book.