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
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
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 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
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
But then it got adopted by non-OOP languages:
- C (1973) had compilation units. The public interface goes to the
.hfile, and the implementation stays hidden in the
.cfile. 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()
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()
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
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.