Object-Oriented and Functional Programming
Object-Oriented programming has the perception as being the traditional and industry standard paradigm of programming for software engineering projects. Meanwhile, Functional programming has the perception as being either the new hotness, the non-traditional, or the academic standard paradigm of programming (depending of course on the blog post, person, or book that you’re encountering). But what does any of that really mean? A case of “nobody ever got fired” for using an Object-Oriented programming language? Is Functional programming really The Future™?
What is Object-Oriented programming and why do we use it
Getting clean definitions for paradigms is always a chore. You need to be both specific and abstract; inevitably there are definitions that are widely used that are inconsistent and definitions that are consistent but that nobody will agree with.
The oft quoted definition for Object-Oriented programming is a programming language that has Encapsulation, Inheritance, and Polymorphism. Although, practically it looks like a language that has a
class keyword that allows you to define object types. Regardless, the arguments immediately flood in. Encapsulation is effectively modularity but limited to objects, so is this really Object-Oriented? Haskell has a form of polymorphism using type classes, and it is a tortured argument indeed that defines Haskell as an Object-Oriented programming language. Additionally, the major players in Object-Oriented programming languages (for example Java and C#) have constructs like
while loops and
switch statements, which find their origin in decidedly non-Object-Oriented contexts. Remove them and these programming languages would still be Object-Oriented, but nobody could actually use them to accomplish anything.
Of course, just because Object-Oriented programming languages contain constructs found in other paradigms doesn’t mean that we can’t define Object-Oriented programming as having those shared constructs. Afterall, pretty much every programming paradigm has the shared aspect of “runs on a computer” and we don’t begrudge them for sharing that aspect. So, let’s draw a line in the sand, but not take it too seriously.
Object-Oriented programming languages have:
• Some way to define objects (typically this is the
• Some way to hide internal behavior of objects (Encapsulation)
• Some way to share object definitions (Inheritance)
• Some way to use an object without knowing its exact definition (Polymorphism)
Now, why do we use Object-Oriented programming languages? To some extent, we use Object-Oriented programming languages because we use Object-Oriented programming languages. Logistics can be a pretty strong argument for using a programming language. If all the major programming languages that are supported by large industry players and communities are Object-Oriented, then it makes sense that we should use them over alternatives. However, how did we get into the scenario that we’re in right now where most well supported programming languages are Object-Oriented?
There’s an intriguing talk by Richard Feldman entitled Why Isn’t Functional Programming the Norm. Feldman reviews the history of Object-Oriented programming languages and concludes that chance and circumstance has led to our current environment of Object-Oriented programming being the norm. Considering the next section, it should come as little surprise that I find Feldman’s talk compelling.
The failure of Object-Oriented programming
Let’s be real for a second. If I invented a programming paradigm that was as widely used as Object-Oriented programming is, then I would be pretty happy regardless of how much of a “failure” it was. The present is undoubtably Object-Oriented; even if there is some reason to believe the winds of change are upon us, Object-Oriented programming has had quite the impressive run.
But how has it failed us? Let’s take a look at a simplified analogy. NAND logic gates. NAND logic gates are a universal logic gate. This means that all other logic gates (AND, OR, XOR, NOT, etc) can be constructed by using only NAND gates. So let’s drag this concept over to Boolean logic in programming contexts. Often a program needs to make a decision predicated on a complicated scenario. And programmers reach for Boolean operators to construct the logic that dictates what decision the program makes. This AND that OR that AND NOT this. We can replace these Boolean operators with a NAND operator.
So the following Boolean expression:
(not a or b) and c
nand( nand( nand( nand( nand(a,a), nand(a,a) ), nand(b, b) ), c), nand( nand( nand( nand(a,a), nand(a,a) ), nand(b, b) ), c))
NAND is clearly very powerful. You can do everything you want by simply using NAND. However, the comprehensibility of what we’re doing suffers immensely when we rely solely on NAND. Having a series of different operators with different behaviors can actually make things easier to work with.
We defined Object-Oriented programming languages to be languages that have:
class, encapsulation, polymorphism, and inheritance. These features really only focus on code abstraction and code interaction. And they only provide a single technique for abstracting or interacting with other code. An existential relationship. There exists some object that looks kind of like this, but you’re not allowed to know anything about the object specifically.
Now existential relationships are really powerful and you can use it to simulate a bunch of other construct types that you want and need. But it’s the same as with NAND. Things get very complicated and messy. It’s not obvious what construct is being simulated and where. And it becomes difficult to comprehend what’s going on.
But wait Object-Oriented Programming Languages are rarely purely Object-Oriented
An Object-Oriented programming language with only
class, encapsulation, polymorphism, and inheritance wouldn’t be used by anyone. If you look at C# and Java, they started out with
while loops and
switch statements. These are features very solidly in the Imperative programming paradigm. And as time has gone by, other features have been added to Object-Oriented programming languages that definitely come from non-Object-Oriented origins.
Generic type parameters in both class and method definitions (originating in ML). Linq and similar themed function groups (monadic extension methods that have quite a bit in common with SQL). Coroutines and generators (basically a ‘lite’ version of scheme’s call-cc). And higher order anonymous functions (straight from lambda calculus).
These days Object-Oriented programming languages are looking much more like a collection of different paradigms where the most privileged feature is the
Functional programming isn’t really Functional programming either
While you’re never going to find a definition for Object-Oriented programming that makes everyone happy, we can make a pretty safe bet.
class, encapsulation, polymorphism, and inheritance. Things are not so simple when it comes to trying to define what makes a programming language a Functional programming language.
Standard ML and OCaml would argue Algebraic Data Types and Hindley-Milner type inference. Clojure would argue Dynamic Typing and Immutability. Haskell would argue Purity, Monads, and Lazy Evaluation. Scheme would say Higher Order Anonymous Functions, Tail-Call Optimization, and Homoiconicity.
Some of these things are compatible. And some of these things are not able to be brought together.
But as we just saw Object-Oriented programming languages usually come with a lot of features that aren’t Object-Oriented. Similarly, Functional programming language look suspiciously like a catch all paradigm where the most privileged feature tends to be the Function.
Ultimately, the most powerful aspect of Functional programming may be that it isn’t called Functional-Oriented programming. With Object-Oriented programming, we saw that the major features that the paradigm is oriented around are able to solve all the problems we’re interested in. But they aren’t able to solve all the problems in a comprehensible way. Because Functional programming isn’t oriented around the Function, other features are able to gain sufficient traction within the Functional programming languages. This is why Functional programming is so fragmented. Each programming language starts with Functions and then adds other features that allow comprehensible programming.
Object-Oriented programming languages all elevate NAND and only allow the other operators to be used with some difficulty. Meanwhile, Functional programming languages start with a somewhat important AND, but then some of them add a just as usable OR and NOT while others add XOR and IMPLIES.
What do we do in the future (what do we expect and how can we prepare)
The future is already on the way. C#, C++, and Java have all been adding non-Object-Oriented features for quite a while now. With the extreme case of C# being just a few features away from qualifying as an ML derivative. New programming languages like Rust, Go, and Elixir have taken their lead from non-Object-Oriented programming languages with only the faintest of traces from Object-Oriented paradigms present.
As we enter a new era, we can probably expect a more even distribution of features. Object-Oriented features are never going to go away completely because they are useful in some cases. However, non-Object-Oriented features are going to be more accessible and more applicable. Preparing for this is going to be as simple as learning non-Object-Oriented programming languages and determining which features are useful for what applications. This is going to be important because as we saw not all Functional programming language features are entirely compatible with one another. Deciding what programming language to use for a project will require some investigation to make sure that the programming language that’s the best fit for the project is utilized. However, the benefit is going to be code bases that are much more comprehensible, maintainable, and robust.