Functional Programming Is Not a Silver Bullet
The past few years have seen a boost in popularity of the functional programming paradigm. Languages that were used mostly in academic circles for decades are now in broader use amongst programmers. And every couple of months, another functional language hits the news and gets its trail of followers.
Why is that? Functional programming allow for safer and more robust code, in part due to one of its core principles: values are not mutable. A consequence of this is that there is no side effects. We can apply this principle in any language, including in C++, by coding with the least side effects possible.
While it certainly helps putting together a better design of code, it’s important to realize that it’s not the panacea, that this principle doesn’t solve in itself all design issues. Nothing is the panacea anyway, but in this time of gold rush towards functional programming, we could be tricked into thinking it will automatically lead to good design.
Functional programming is known to reduce coupling in code. We’ll briefly go over what coupling is, what sort of coupling functional programming prevents, and how some other dangerous forms of coupling can still sneak in even with functional programming. You want to pay attention to those to preserve the design of your code.
A particular case: Coupling to state
What is coupling? Here is an informal definition: two components are coupled when you need to know what both of them do to understand one (or both) of them. And here, the term “component” can mean pretty much anything in code: from a variable to a function to a class to a module to a service to an application.
It follows naturally that, the less coupling in code, the more understandable code is. Indeed, with low coupling you can examine components in isolation and understand them without having to fit too many things in your head at the same time.
A program needs some amount of coupling to hold together though. For example, one part of the program can depend on an API exposed by another part of the program, and you need to know the meaning of this API to understand the part that depends on it. And this is fine.
But too much coupling makes a program a nightmare to understand and maintain. One example of coupling that can go out of control is when the inside of two components depend on each other. A typical example is when two functions access a common mutable state. This can happen with global variables, but not only. For example, it can happen with two methods on the same object, or with two functions that access and write into the same object that is passed around.
Then if the second function rely on the first one to have changed that mutable state, then both functions are involved in an unhealthy coupling. Indeed, to understand the body of the second function, you need to know what the one of the first function did. And then the order in which you call them start to matter.
But functional programming forbids mutable state in the first place!
Does this mean that it also prevent coupling?
The general case: Coupling to behaviour
In our definition of coupling, the relation between the two components was that the second one needed to know what the first one was doing. In the particular case of setting a state, then yes, functional programming prevents that. But in the general case, functional programming allows to depend on other types of behaviour than setting states in remote parts of the code.
To illustrate this, let’s take a simple example, where we’ll introduce bad coupling without making any side effect.
Say that we have an enterprise system that manages the orders of a company. Some orders are overdue, which means that they should have been paid by customers by now, but they haven’t. We have a function that takes in a collection of orders, and indicates which orders are overdue:
std::vector<size_t> overdueOrders(std::vector<Order> const& orders) { std::vector<size_t> overdueOrderIndexes; for (size_t i = 0; i < orders.size(); ++i) { if (isOverdue(orders[i])) { overdueOrderIndexes.push_back(i); } } return overdueOrderIndexes; }
Since Order
s are large objects (or entities, or whatever non-copyable things) and we don’t want to copy them around, we only return the positions of the overdue orders in the collection. Note that we’re not using STL algorithms such as std::transform
and std::copy_if
here, because we need the position of the current elements. Note that there are ways out of explicitly getting the position, that we reviewed when accessing the current index during a traversal.
Now there is an additional requirement: we need to provide to the user a list of the overdue orders’ numbers. But for the user, orders start at 1. And for us programmers (and for std::vector
s too), orders start at 0.
One (wrong) way to go about this would be to make the overdueOrders
function return positions that start at 1:
std::vector<size_t> overdueOrders(std::vector<Order> const& orders) { std::vector<size_t> overdueOrderIndexes; for (size_t i = 0; i < orders.size(); ++i) { if (isOverdue(orders[i])) { overdueOrderIndexes.push_back(i + 1); } } return overdueOrderIndexes; }
Still no side-effects so far. But here is one last requirement: the user wants to know how much revenue those orders represent, because it’s as much money that they may have to say good bye to.
So we have another function, that calculates the total revenue of a collection of orders. This function also accept a collection of positions to filter the sum on those positions only. We’d like to call it this way:
totalRevenue(orders, overdueOrders(orders));
But since overdueOrders
‘s output starts indexes at 1, we need to subtract 1 to access the right elements in the vector in totalRevenue
‘s implementation. Which is weird:
double totalRevenue(std::vector<Order> const& orders, std::vector<size_t> const& indexes) { double revenue = 0; for (auto const& index : indexes) { revenue += orders[index - 1]; } return revenue; }
This is coupling, and a very bad one. Indeed, now that totalRevenue
is coupled to overdueOrders
(which was itself coupled with the UI because of indexes starting at 1) several things have become more difficult:
- understanding
totalRevenue
: if you take it in isolation, this “index - 1
” makes no sense, - reusing
totalRevenue
: we need to somehow pass it indexes that start at 1 (or worse, indicate whether the indexes we’re passing to it start at 0 or 1)
So here is our counter-example: we’ve built function that don’t have side-effects (at least from their callers’ perspective), yet have tight and unhealthy coupling.
Maybe this example reminds you of familiar code, or maybe it feels alien to you. Either way, what it illustrates is that if the output of a function is crooked in some way, then other functions have to bend themselves to uncrook it to use it. This is coupling, and this is compatible with functional programming.
It’s not about functional programming
If you’re a functional programmer, you may be outraged at this point (Quentin if you hear me…). Indeed, in idiomatic functional programming we’d probably write something like orders | filter(overdue) | sum
to compute the total revenue, and something just as elegant to deal with the indexes. And I agree with that. The point of this article isn’t to show that functional programming necessarily leads to coupling.
The point is to show that functional programming doesn’t automatically lead to good design. There are ways to reduce the coupling here while staying in the functional programming paradigm, and there are ways to reduce it in the object-oriented paradigm. The idea would be in both case to moves the responsibility of making indexes starting at 1 closer to the UI, so that our business logic doesn’t know about it.
Decoupling and distributing responsibilities are fundamental aspects of design, that transcend any one programming paradigm. Don’t be tricked into thinking that functional programming, or any other popular paradigm coming before or after it, will take care of thinking about good code design instead of us.
You may also like
- Which Programming Paradigm Gives the Most Expressive Code?
- How to Access the Index of the Current Element in a For Loop
- 50 People’s Opinions About How To Get Better As A Software Developer
Share this post!