Despite its reputation as a mathematical fad, functional programming had the greatest return on investment of all the computer science I studied:
To show these properties, we will go backwards:
Bugs happen when a computer runs instructions you wrote but not those you meant. A bug, therefore, manifests itself in the form of an unwanted effect (or worse, its absence, much harder to debug). The process is as follows:
The branching factor makes this process exponential and, therefore, very time-consuming.
The problem, essentially, lies in the changing nature of the state: many instructions can change one variable, and because of the nature of computation, there is sometimes no other option than to replay the execution.
Functional principlesLet us, therefore, declare the following principles:
Since code only transforms data, like a function, we call this kind of programming functional.
While these principles, when properly followed, eliminate bugs, but is it not limiting?
Imperative exampleLet us try a simple example: reversing a linked list. Here is an imperative solution, where we invert pointers:
You can edit and run the code!
Phew: it works on our small example. But the logic is complex, particularly the reassignment of pointers. If there was an error, where should we look? The initialisation of previous
and current
? Is the order of the instructions in the loop correct? Etc.
The only way to be sure is to test all the cases:
null
,NodeList
,NodeList
.What is trivial on one small example becomes impossible at a larger scale. The developer lives in the terror that, sooner or later, a bug appears, and he will have to sift through all this imperative logic. So, let us make that code functional instead.
Functional exampleSince we operate on data, let us look at the shape of our input, and what we need.
ListNode
is null
, it means that we have reached the end of the list.tail
. We will see later how to get it.Half of our work is done! What about the second case, when our ListNode
is not null
? It means that we are iterating, currently examining a node of our list. Our tail
parameter contains everything we have seen so far, in the right order. So, we will build the correct tail
for the next
node:
Finally, we can continue the iteration:
Done! Here is the complete function:
Much cleaner. One has to get used to read and write code in this style (the greatest paradigm change in my programming career!) but the benefits are, actually, even greater.
TypingWhen we were debugging, we read the code while keeping properties about the state of memory in our head.
On this line, the list has > 0 elements. On this line, it has all the elements it had plus the new one. Etc.
With functional programming, since variables do not change, we can directly assert these properties in our code. That is typing. More specifically, what is known at compile time is called static typing.
Then, the compiler verifies and composes all these little assertions to prove that our result is correct: it is what we meant.
Here is our functional code, with the most extensive typing:
For someone who is not used to types, this is certainly frightening. Types are longer than our function! (Smarter type systems than TypeScript might be able to infer them automatically. This area of research is particularly active.)
While the example is running correctly, let us see what the types tell us.
Type instantiation is excessively deep and possibly infinite.
Indeed: when someone gives us a circular linked list, we will cycle indefinitely. It showed us a bug before running the code.
In addition, you can hover on the variable test
above to see the inferred type: TypeScript knows that it has the correctly-ordered sequence 🥇 🥈 🥉 before running the code!
It is only possible thanks to the principles listed above. We can define much more precisely the pre- and postconditions of our function. With this approach, you do not have to keep properties about your code in mind. Our code is proven safe: the focus is less on tests, but they can still be useful when the type system is not powerful enough. However, instead of fighting the type system, we usually try to make invalid states impossible in the definition of the shape of our data.
Personally, I find types especially useful during:
For a further study of the power of type systems in a functional context, you can check generalised algebraic datatypes and dependent types.