The current state of web development is best characterized not only by the myriad of problems developers face, but also by the vast number of ways we can go about solving those problems. But are they all created equal? As the technology that powers our solutions continues to drive forward at an almost dizzying pace, it is the responsibility of developers to ensure that our solutions are scaleable for today, tomorrow, and the future. One such method of software development — one of the oldest, but until recently not widely adopted on the web — challenges us to craft our applications in this way.
Enter Functional Programming. First embraced in the 1950s with the advent of the LISP programming language, this programming paradigm enables developers to create reliable, testable code that can be reasoned about easily, all while sidestepping the common pitfalls of more standard approaches to programming. Before we dive into what Functional Programming is, let’s quickly take a look at why we might decide to approach our solutions in this way:
Simply put, functional programming:
- Allows for pure functions that are reliable, testable, and simple to understand.
- Enables first class functions that can be composed, increasing code reuse.
- Makes programs as a whole easier to reason about and debug due to active avoidance of data mutation.
What is functional programming?
As summarized on Wikipedia,
Functional programming is a programming paradigm — a style of building the structure and elements of computer programs — that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.1
Thinking of computation evaluation as mathematical functions is a mouthful, but it’s actually a huge simplification of how developers might normally approach programming. At its core, a mathematical function is just an expression that returns some output when given some input. If you add one and one, you will get two — always. The greatest common denominator of eight and 12 will always be four. Mathematical functions always return identical output, given the same input. Another name for this is referential transparency.
Referentially transparent functions are useful because they reduce complexity and eliminate uncertainty. Every mathematical function is referentially transparent; many functions in our applications are not, but there is no reason many of them cannot be! Let’s see an example of a referentially transparent function in action.
Here, our `reduce` function takes two arguments: an array, and a function.2 The `reduce` function cycles through the array, reducing it down to one number by adding the current number to the previously computed total. The interesting thing is the output of this function call. It’s going to be 10, and it’s always going to be 10, as long as you pass it an array containing the numbers one through four and a helper function that adds its arguments. What about a function that is not referentially transparent?
The increment function here is called with the same arguments two times, and each time, the output changes. That’s because this function is referentially opaque; it does not return the same output given the same input.
So why referential transparency? Because it reduces function complexity. If a function can always be relied on to return the same output given the same input, then that function can be reliably tested and it will never surprise you. But there is another, more important reason why referential transparency is so powerful. Referentially transparent functions are only concerned with themselves; the outside world plays no role in their work, and their work has no impact on the outside world.
When you are sick you go to the doctor, who will prescribe you with medication to make you feel better. However, the medicine might have side effects that cause you other pain and discomfort: The pain reliever you take for your headache may result in a stomach ache a few hours later.
Similarly, in programming, a side effect is when a function performs some action that has some impact on the program at large. Restated, when a function causes something to happen outside of its own scope, that function is said to be causing side effects. We don’t like side effects in real life, and we shouldn’t like them in our programs — they should be avoided whenever possible.3
When a function is referentially transparent and does not create side effects, it is called a Pure Function. A pure function has one essential trait: It does only one thing, but it does that one thing very well. Like Legos, or the Unix Philosophy, building out programs with many small, pure functions ensures expressive, non-complex programs.
We can take it a step further and say that all pure functions are Black Boxes. In a black box, the internals do not matter. To the observer or consumer of a black box function, what goes on inside does not matter because that function is referentially transparent and does not cause side effects. There is never any worry about what might happen if a black box function is called: It will not effect the program at large, just return some data based on some given arguments. Black box functions are what allow popular functional methods such as map, filter, and reduce to be so expressive; instead of describing implementation detail, you are instead describing how you want to represent the data — the implementation is abstracted away into the black box.
We’ve seen how pure, black box functions are immensely useful, safe ways to structure our programs. However, with functional programming, these already-powerful pure functions are made even more extendable because they are also first-class citizens.
If a programming construct is a first-class citizen in a given language, it can be utilized in a multitude of ways. This flexibility enables those constructs to be used in expressive and extendable ways; this is where the real power of functional programming comes from. For a function to be ‘first-class’ it must meet several criteria:
- It can be referenced by an external name (Variable assignment)
- It can be passed as an argument to another function (Callbacks)
- It can be returned from a function (Composability)
Here, composition enabled us to describe a new behavior (adding one, and then multiplying by two) without having to explicitly write the implementation. This is only possible because we can pass and return functions to/from other functions. An additional benefit of composition is that it allows for function currying.
Currying allows us to use already implemented functions by calling them without all their arguments to define more specified behaviors. In this case we are using the already defined add function to describe the addTwo function. addTwo just returns a call to add with the second parameter already set as two.
We’ve already seen the elegance and expressive power of functional programming. Our code is already shorter, simpler and less prone to bugs. But what about the data that our functions act on? What happens if our data changes during the course of program execution? Once again, functional programming’s answer to these questions is largely the same: Keep it simple! How does functional programming keep data flow simple? By not changing the data at all.
The truth is mutable data greatly increases program complexity. If data is being passed between functions, and changing as it moves around between them, then our ability to pinpoint exactly what (or where) some data is becomes largely impossible. Immutable data on the other hand, reduces complexity by restricting data from changing. If some function is acting on some immutable value in memory, then you can be confident that your function is acting on exactly what you expect it to be acting on.
Mutable data might not be a tremendous problem (or even a noticeable one) until things start to scale. It’s easiest to understand this very real problem by thinking about it concurrently. If two functions are acting on some data concurrently (ie, at the same time), and one or both of those functions is changing that data, it’s not hard to see how things become problematic quickly. Indeed, at scale this problem will only become worse. But how do we get to the data we want without changing the data we have?
Array.map is a method which does not mutate data. As shown above, even though we are mapping over the foo array and adding one to each element, the value of foo itself does not change. Instead of mutating the array itself, Array.map returns a new array with the mapped changes.
In the above example, the pushNumToArray function alters the data inside the passed array. Think now about what might happen if pushNumToArray and some other function were acting on the array bar at the same time. Can you say for sure that the other function would be acting on bar without the five added to it?
- Wikipedia, https://en.wikipedia.org/wiki/Functional_programming
- This is not the native `Array.reduce` method. This is perhaps the `reduce` that one might import from a functional library, such as lodash or Underscore. This was chosen to make the definition of referential transparency clear.
- Side effects are necessary for relevant, useful applications. A function that causes an image to appear on a screen is creating a side effect. Without the side effects, there would be no purpose to our applications. However, we should be prudent in ensuring side effects are limited to the outermost layers of our programs.
- Mutation inside a pure, black box function is perfectly OK. Why? Because as we said, pure functions are black boxes — what goes on inside of them doesn’t matter to the outside world.