Functional programming is a programming paradigm that has its roots in mathematics and is the source of a lot of the foreboding terms and complex dialogue. In fact, the term functional programming has a myriad of connotations and definitions, creating hence a multitude of misunderstandings between programmers. However, under that thick crust of academic theory, one can find simple and easy-to-apply concepts which will make code easier to test and easier to reason about while making it more concise.
What is functional programming?
In order to unveil what defines functional programming let’s start with things it is definitely not:
- Functional programming is not a language. There’s a common misconception that there are “functional programming languages” like Elixir, Haskell, and Scala and that functional programming concepts are only useful in those languages. Whilst it’s true that these languages do encourage writing functional programs (and in some cases, enforce the rules), functional programming is not unique to them and the practices are useful whether you’re writing ML or PHP.
- Functional programming is not just for academics. Although the roots of functional programming are deep-seated in mathematics, the principles can be applied by any engineer and improve the quality and predictability of that engineer’s code.
Just to clarify, any of these things are bad — if you can write Haskell, or use Vavr in your Java projects, or have a background in mathematics, that’s a great thing — but that they are not necessary to apply the principles of functional programming in your day-to-day code. Yes, even if your employer insists on using Java 5, PHP 5, or Perl 5. Why is it always version 5?
With this in mind, what is functional programming? Let’s break it down into its core principles:
Side Effects & Pure Functions
To understand functional programming first, we need to understand side effects. A side effect is when a function relies on, or modifies, something outside its parameters to do something. For example, a function which reads or writes from a variable outside its own arguments, a database, a file, or the console can be described as having side effects.
A function is pure if, given the same inputs, it a) always returns the same output and b) does not have any side effects. At first, it may not be apparent why pure functions are preferable, but take a look at this example (written in Perl, which is about the furthest thing from a “functional programming language” that I can imagine):
The ‘triple’ function is much easier to unit test, because its result depends solely on its input. It’s also much easier to run in parallel because it doesn’t rely on access to shared state. Most importantly, it’s predictable: triple(5) will always return 15, regardless of how many times we run it or what the state of the surrounding system is saying.
But In The Real World, We Need IO
An application isn’t really useful if it can’t interact with the outside world, though, is it? So how do we handle side effects like printing to the console, reading & writing from databases, generating random numbers, etc? The answer is that we write our business logic as pure functions, and move side effects to the edges of our process — that is, instead of reading from our database in the middle of our process; we do IO, then we have a core of business logic, then we do IO again.
In the Java world, this is already considered best practice. Alistair Cockburn explains in his introduction to the Hexagonal Architecture that business logic should not deal with IO, and these should be kept at the edges of the software and implemented with interfaces to make them easier to test. Uncle Bob Martin’s Clean Architecture is, in its essence, a guide to keeping side effects at the edges of your system designs.
So, the goal of a functional programmer shouldn’t be to eliminate IO, but to move it to the edges in order to keep our business logic pure, composable, and testable.
Referential Transparency & Data Transformations
‘Referential transparency’ is a fancy way of saying that you can replace the function with its result, and vice versa, without incurring side effects. (2 + 2) * 4 === 4 * 4.
From the simplest calculator to the most complex data science model, the purpose of programming is to take an input or inputs (whether that comes from a user, a sensor, another application, or anywhere else) and transform it into an output. By thinking about programming this way — “I have this input and I want this output” — we can break our code down into functions, plumbed together into pipes, which take our input data and apply the requisite transformations to turn them into output data. That plumbing relies on referential transparency.
Once the preserve of niche academic languages, immutable values have now made their way into most mainstream languages. Whether it’s const in ES6, the final keyword in Java or val/var in Kotlin, most languages have embraced a future wherein we don’t modify data structures, but copy them.
This might seem counter-intuitive: why would we want to copy something instead of changing it? The answer lies in unintended effects:
This might seem like a contrived example — everyone knows that object assignment duplicates the reference, right? In complex code, though, it’s actually quite a common issue. A reference will be duplicated somewhere in the code, then passed into another function that doesn’t realize it’s working on a shared reference, and then it’ll update the state of an object being used elsewhere in the system. Immutable data structures allow us to avoid this possibility entirely.
Haskell actually goes a step further and doesn’t allow values to be reassigned. If you say x is 3, then x is 3 for the remainder of the lexical scope and there’s no way to tell the program otherwise.
In imperative and object-oriented programming, it’s common to use loops (for, while, forEach) which have side effects (increasing the iterator, changing the ‘while’ variable, or modifying the structure that’s being iterated over). In functional programming, we prefer recursion. A recursive function is a function which calls back to itself with modified input values (otherwise we’d have an infinite loop, which is predictable but not very useful), until it ‘finishes’ processing.
The most common recursive functions to replace loops (all of which are already implemented in most languages) are:
- map, which takes an input collection and applies a function to each element in the collection, then returns an output collection of equal size (note: the input collection is immutable, so it will not be changed in the process);
- filter, which takes an input collection and a predicate (a function which returns a boolean), and tests every input against the predicate, then returns only a collection of the elements which return true; and
- reduce (or fold), which takes an input collection and applies a function which takes an accumulator and a value and turns it into a single value, eventually returning a single value (like a String or an Int).
All of these recursive functions are examples of higher-order functions, meaning that they are functions which take functions as parameters. This is commonplace in functional programming because it allows us to write highly reusable code and compose terse data pipelines far more easily.
Here are some examples using recursion for calculating employee bonuses:
Note that none of the functions modify state, even employee-list doesn’t change throughout the operation. We could run the commented operations on line 24 and 27 until we’re blue in the face, but that wouldn’t change the results.
One tool that functional programmers like to use, and is implemented in many languages, is context wrapping. This is when we take a value (or a function) and wrap it in a box. One you might be familiar with is Maybe/Option/Optional. Maybe Int can have one of two results depending on the context; it can be Just Int (i.e. Just 5) or it can be Nothing.
The language used can differ — Just/Some, or Nil/None/Nothing — but the idea behind context wrapping is that we can ‘box up’ two or more types of result into one value, and only unbox them only when we need to.
Context comes in a variety of forms, most of which have arcane mathematical names but are very easy to understand (you’re probably already using them). Aditya Bhargava did a much better job than I could possibly do at explaining the different kinds of wrappers explaining the different kinds of wrappers using diagrams and plain English, but the core ones are:
- Functors, which are context wrappers which define how functions can be applied to their boxed values;
- Applications, which are context wrappers which define how boxed functions can be applied to their boxed values; and
- Monads, which are context wrappers which define how a function which returns a boxed value can be applied to their boxed values.
Maybe, in Haskell, is an example of all three. It defines how functions can be applied to its value, how boxed functions can be applied to its value, and how functions which return boxed values can be applied to its value. I really recommend reading Bhargava’s piece on the subject, as the diagrams make this a lot more digestible, but if you’ve worked with Options, Futures, Eithers, or anything similar which wraps a value, chances are you’re already using context day-to-day (or you’re a banker, in which case, these terms have very different meanings).
The value of context is twofold:
- By putting the logic about how to handle null values, errors, and other non-golden path circumstances into the boxes, we prevent ourselves from having to think about them in all the functions that handle them — no more if (x === null) || (x === “”) guard clauses, and no more “you forgot to test the null case” on pull requests;
- We can pass the context all the way through our data transformation ‘plumbing’, and handle the edge cases at the edges. Instead of having to determine what to do when an exception is thrown, or a calculation returns null, and re-plumb our logic on the fly, we can surface the error to the edge and handle it there, keeping as much pure code as possible.
A lot of what’s described here probably isn’t that different from what you already do, because the core functional programming principles have been ported over as best practices to object-oriented and imperative programming languages. If that’s the case, hopefully, you now have a better idea why those are best practices, and if not, hopefully, you’ve got the information you need to start trying!
Functional programming is, at its core, about moving IO away from your business logic in order to create a core of pure functions, because they have so many natural benefits:
- Easy to compose;
- Easy to test;
- Easy to reason about;
- Easy to predict.
If you want to learn more about functional programming, I recommend reading Learn You A Haskell, a free book by Miran Lipovača which really helped me to better understand the principles of functional programming. Even if you never intend to use Haskell outside of learning it, experience with a very strict functional language will give you a new way to look at, understand, analyse, and write software.