Functional programming is not a new idea, even though its popularity is a relatively recent phenomenon: the groundwork for many of the existing FP languages was laid in the 1930’s, by a mathematician named Alonzo Church, and his lambda calculus (if you’ve heard the term “lambda expression” – yes, they are related).
Also in the 1930’s, another model was developed – the Turing machine – by another mathematician, Alan Turing. This formed the basis of imperative, procedural and, eventually, object-oriented programming languages.
Those two models (and a third one that is still waiting for its time in the spotlight) were eventually proven equivalent by the Church-Turing thesis, meaning any computation that can be expressed in one can be expressed in the other. Of course, this equivalence only says what is possible, not what is practical – you can do anything in any programming language, yet this doesn’t mean it’s a good idea to just pick a language at random when starting a project!
In this article, we will see whether functional programming might be a good idea when it comes to blockchain and smart contracts, and we will dispel some myths while we’re at it.
The Expression Problem
Functional programming and object-oriented languages differ in how they approach “the expression problem”: the problem of adding new data types and new operations to existing code, in a type safe way, without recompiling the program.
In object-oriented programming, it’s easy to add new data types, for example by inheritance (subtyping), but it’s not easy to add new operations (methods) that work with existing types. In functional programming, it’s the opposite: it’s easy to add new operations, but not easy to add new data types.
Let’s use a popular example: imagine we’re writing a graphics drawing program. We support circles and triangles, and two operations on them: move and resize.
How you might do this in an OOP language, you define an abstract class called Shape
, and two classes that inherit from it: Circle
and Triangle
. The abstract class would declare two methods, to be defined in the concrete classes: Move(int x, int y)
and Resize(float percent)
.
If now we want to add rectangles, this is rather easy: just define a class Rectangle
that inherits from Shape
, and define the needed methods. However, if we want to add a new operation, say Rotate(float degrees)
we’d have to add a new method to the abstract class and all concrete classes – almost all of our code would need to be recompiled. One solution is to use the visitor pattern, but this has the same issue as the FP solution that we will show next.
So, how would we do this in a FP language? We’d have a data type like Shape = Circle | Triangle
(read “a Shape is a Circle or a Triangle”), and for the operations we’d have two functions that each “pattern match” on the possible cases (think of it as overloading in OOP – it lets you “dispatch” and run different code depending on the types of the parameters). To add a new operation, like the rotation operation above, we simply define one more function – however, to add a new kind of shape, we’d need to update the
Shape
data type, and all functions that use it.
That said, many languages have some solution to this FP/OOP dichotomy: for example, extension methods in C# let you add new methods to existing types (for example, LINQ extends IEnumerable
in this way), and type classes (no relation to classes from OOP) in Haskell let you add new data types that existing operations can work with (it basically works a bit similar to an interface in OOP).
So, it’s possible for a language to have both types of extensibility demanded by the expression problem, but they still differ in which is the “main” way and which is mostly tacked on.
Functional Programming vs. Procedural Programming
If we look at object-oriented programming as “procedural programming with classes” (which is a point of view that might offend some OOP purists, but is not particularly wrong when it comes to how OOP is used in the industry), it might make sense to compare functional programming and procedural programming.
In procedural programming, the main means of abstraction is the procedure: a sequence of instructions that, when executed, change some variables. Conversely, in functional programming, the main means of abstraction is the function: an expression to be evaluated, which can be replaced with its result; there’s no state, no changing of variables.
For example, here’s a procedure that sums a list of numbers (in JavaScript):
function sum(list) { let total = 0; for (let i = 0; i <= list.length; i++) { total += list[i]; } return total; }
The result is, as expected, NaN
(“Not a Number”).
Wait, NaN
? Oops, there’s an off-by-one error in our program, so JS tried to access an array out of bounds, got an undefined
, and tried to add this to a number!
Here’s how we might do this the functional programming way instead:
function sum(list) { if (list.length == 0) { return 0; } else { [x, ...xs] = list return x + sum(xs); } }
We use some modern JS features to “deconstruct” the list into a first element, and the rest of the elements (so x
is a number, while xs
is a list of numbers).
Here’s the same program as above, written in Haskell (a popular FP language):
sum [] = 0 sum (x:xs) = x + sum xs
There’s no room for errors in that program! It’s a higher-level description of what we’re trying to do, so there are fewer things that could go wrong.
What does this program mean? Well, the equals sign means the same thing as it does in math: it means “is equivalent to”, “can be replaced with”. If we have 2 + 3 = 5
, this means instead of 2 + 3
we can write 5
, it’s the same thing.
So the first line means we’re allowed to replace sum
, when used with the empty list as an argument, with the number 0
– while the second line is for the non-empty list, we replace it with the first element plus the sum of the rest of the elements.
Let’s see how we might evaluate an expression like sum [1, 2, 3]
.
We can’t use the first definition, since [1, 2, 3]
is not the empty list. However, we can use the second one: sum [1, 2, 3] = 1 + sum [2, 3]
We can use the same substitution again: 1 + (sum [2, 3]) = 1 + (2 + sum [3])
And again: 1 + (2 + (sum [3])) = 1 + (2 + (3 + (sum [])))
Now, the first definition matches (the one about the empty list): 1 + (2 + (3 + sum [])) = 1 + (2 + (3 + 0))
Let’s also replace the additions by their results (using the definition of the +
function, which does exactly what you’d expect): 1 + (2 + (3 + (0))) = 1 + (2 + 3) = 1 + 5 = 6
And there we have our answer: sum [1, 2, 3]
can be replaced with (“evaluates to”) 6
.
By the way, if you’re familiar with mathematical induction, you probably noticed something familiar in the FP code: we have a base case (the sum of the empty list is 0
) and an inductive step (the sum of a non-empty list is the first element plus the sum of the rest of the elements).
Myths and misconceptions
Now, before you think that functional programming will solve all your problems: it’s still possible to introduce bugs (e.g. you can solve the wrong problem in any language), and there’s a price to pay, in that sometimes it requires more up-front thinking (e.g. to come up with that recursive definition of sum
). However, we’re talking about smart contracts here, so a bit more effort to come up with programs that are more likely to be correct is a pretty good deal.
In fact, many blockchain projects, like Cardano, are quite adamant about smart contracts basically requiring functional programming and formal verification (the practice of using mathematics to prove that a certain program has certain properties, e.g. for a wallet smart contract, proving that no possible execution of the code results in a user withdrawing more money than they put in).
So, here’s the myth: that this formal verification, these formal proofs of correctness, are only easy to write for programs written in FP languages. The reality is, this is only true if you’re writing the proofs by hand – however, there are tools that can help with the verification of OOP code, including tools for Solidity specifically (e.g. check this out – Formal Verification in Solidity).
Conclusion
So, back to our question: which is better for blockchain and smart contracts, functional programming or object-oriented programming? The answer shouldn’t surprise us: it depends. Some people find one works better for them, others – the other. I encourage you to try both (and it’s possible to combine them to some extent, so you can “mix and match”).
Would “The DAO” have gotten hacked if the smart contract was written in a FP language, where it would have been more obvious that the code is working with attacker-controlled state? Maybe not, if people had actually realized that “sending” the Ether actually calls a function, and the rest of the smart contract continues working from that “dirty” state. However, considering how FP is often used in blockchain, where most of the code follows FP style, with immutable values, but when it comes to the blockchain itself, it often works procedurally, with the ledger as a huge, shared, mutable variable – maybe only formal verification would have helped (but this is a topic for another article).