July 2015
Enough with paradigms!
Paradigms generally sound like “[something] programming”. Procedural programming, structured programming, functional programming, object oriented programming, logic programming, generic programming, tabular programming (yes, I’ve heard that one)…
Intuitively, a paradigm is a set of tools and practices. Procedural
programming uses procedures to separate the different parts of the code.
Structured programming avoids the goto
statement in favour
of more disciplined control structures such as if
and
while
. Functional programming tries to avoid or segregate
side effects and use higher order functions liberally. Object oriented
programming… I don’t know. (Well, nobody does.)
The problem is getting stuck in your paradigm of choice. Like
sticking to structured programming to describe a finite state automaton
(those are easier to describe with lots of goto
statements), or insisting on OOP to write a parser.
This is most visible with OOP, because of its sheer ubiquity. I’ll use it as an example.
The OOP myth
First we had procedural programming. Then OOP came and made things better. It increased modularity, flexibility, and code reuse. Nowadays, Many programmers choose OOP for everything, right tool for the job be damned.
This hype isn’t entirely unfounded. OOP did have 3 big advantages: encapsulation, dynamic dispatch, and inheritance. But then a few things happened:
- Encapsulation got adopted in non-OOP languages.
- Dynamic dispatch existed even before OOP.
- Inheritance turned out to be mostly a bad idea.
Encapsulation: Stolen!
Encapsulation is about separating public interface from implementation detail. By accessing stuff through the interface alone, you are prevented from doing a number of mistakes, such as breaking a object’s invariants. This lets you build abstractions (stack, queue, search tree…) without explicit language support.
In C++ and Java, encapsulation simply means distinguishing
private
members from public
methods. In
Smalltalk, instance variables are all hidden, and you have to use
methods. The first language that let you hide objects’ attributes was
Simula (1967), the first object oriented language.
But then it got adopted by non-OOP languages:
- C (1973) had compilation units. The public interface goes to the
.h
file, and the implementation stays hidden in the.c
file. With forward declarations, you can even hide data type declarations. - Modula 2 (1979) had, well, modules. It provided encapsulation through an export mechanism: anything not exported was hidden.
- Modern incarnations of ML (standard ML, Ocaml, F#) and Haskell all have a module system that provides encapsulation.
Note that encapsulation as described here is not the only way to separate interface from implementation. That separation was already present in the humble function. Functions can be called, but not inspected. You can observe their behaviour, but you’d have to read their source code to see their implementation.
Dynamic dispatch: Predated!
Dynamic dispatch is about parametrizing over behaviour. You invoke a piece of behaviour, but you don’t know which at compile time. That decision is deferred at run time. Here is an example in Python:
def foo(bar):
bar.baz()
The function foo()
does not know where the method
baz()
comes from at compile time. That’s determined at run
time, when an actual object (containing an actual baz()
method) is provided. Note that C++ and Java can basically do the same,
there’s just more boilerplate involved (they use inheritance and virtual
tables instead of duck typing).
If you have first class functions, the behaviour you invoke doesn’t have to be hidden behind an object. Again, in Python:
def foo(baz):
baz()
I won’t insist on why dynamic dispatch is such a big deal. Suffice to say, it was the advantage of OOP over plain procedural programming. This time however, Simula wasn’t the first to provide it. Lisp already had first class functions in 1958. And now, every modern language supports some form of dynamic dispatch (either through virtual methods (C++, Java), duck typing (Python), prototypes (Self, Javascript), or first class functions (Lisp, Javascript, Python, ML, Haskell, Java, C++…). Even C has limited support for dynamic dispatch, thanks to its function pointers.
Inheritance: Rejected!
Inheritance is the only significant feature of OOP that hasn’t been widely adopted elsewhere. For good reason. It doesn’t really help with code reuse, and it breaks encapsulation. Now, composition is preferred.
I know of one solid use case for inheritance: enabling dynamic dispatch in statically typed class based languages, thanks to subtype polymorphism. Though even then, nearly every modern language supports closures, making subtype polymorphism mostly unneeded.
And if you’re a language designer, nothing stops you from separating implementation inheritance from interface subtyping. Like Java’s interfaces, except any class may be treated like an interface. You would get the benefits of subtyping without the problems of inheritance.
Another nice use case I stumbled upon for inheritance was formal
grammars. I think this works because in this case, there is no
encapsulation to break: every rule is effectively
public
.
Adopted out of existence
On the one hand, I think OOP should die. It is overused, and often encourages bad practices. Yet I still meet programmers who disparage code for not being of their favourite flavour of OOP. Good code does not mean “OOP”, (nor “Functional” for that matter). It mostly means short and modular.
On the other hand, encapsulation and dynamic dispatch are wonderful. Even inheritance is useful sometimes. I don’t want to throw out the baby with the bathwater.
If OOP ever dies, I want to feast on its corpse.
Kill all the paradigms!
Then feast on their corpses.
Who cares about functional programming when you just want referential transparency? Who cares about generic programming when you just need templates? Who cares about logic programming when you can pick an inference engine?
Some would talk about “multi-paradigm” programming, but that conveys the wrong idea: in practice we don’t combine paradigms, we cherry-pick mechanisms. That’s not using multiple paradigms, that’s just not giving a damn.
Which is a good thing: when our mind is freed from the shackles of paradigms, we can stop doing OOP, FP, or whatever is trendy, and focus on writing good code instead.
Mechanisms over paradigms.