Hello, OOP

Someone shared Charles Scalfani’s article about why they were abandoning Object Oriented Programming and moving to Functional Programming. The points Charles makes are:

  • Inheritance and hierarchies tend to be fragile and difficult to work with;
  • Making variables private in an object doesn’t help that much; and
  • Polymorphism isn’t exclusive to OOP;

These experiences probably resonate with many people who’ve worked with OOP codebases. In any language. So I’m not going to say that Charles is wrong, but I’d like to offer a different perspective of the ideas in OOP here. And maybe that’ll help someone learn something new in the process, who knows?

What’s OOP?

Just so we’re on the same page, let’s define Object-Oriented Programming as a paradigm where you express programs in terms of objects. Objects are first-class values (you can pass them around, store in variables, etc.) that can define their own behaviour (through methods or messages). That’ll be the entire definition, and it includes all languages people have once labelled OO.

(If you’re interested into a more detailed discussion of the term, read Cook’s blog post on trying to define the concept of objects and OOP)

Within this paradigm we have several other concepts:

  • Encapsulation;
  • Inheritance;
  • Classes;
  • Polymorphism*;

None of these concepts is exclusive to OOP, but that’s where people are most likely to encounter them. I would not say that any of these are “pillars of OOP” either, but a lot of people seem to think that some are, and I feel like this is one of those cases where the descriptive nature of language makes things very confusing, by assigning the same term to different notions of a concept, or even different concepts altogether!

With that out of the way, let’s talk about Charles’ points.

“Inheritance is fragile”

It is. And it’s particularly troublesome when a programming language merges the idea of implementation inheritance with the idea of subtyping. Sadly, most typed OOPLs do this. TypeScript is one of the few exceptions, but only because it uses a structural type system — something is a subtype if it has at least a particular set of properties, and names don’t matter.

The problem with inheritance, however, is neatly captured in its entirety by something that should be at the heart of all object-oriented design, and that’s Liskov’s Substitution Principle. She came up with a very simple principle to govern the correctness of programs that rely on subtyping: “if A is a subtype of B, then A and B must be interchangeable in all contexts a B is expected.”

This means that overriding functions from a base class is not allowed, unless you can make it behave exactly the same way. That sounds restrictive? It’s supposed to be restrictive! You’re not supposed to change the behaviour of base classes.

“But…” you say, “doesn’t that defeat the entire purpose of inheritance?”

Kind of. Luckily, as explained in The Early History of Smalltalk, nobody’s really married to it, as a conce—erm, most people aren’t married to it as a concept.

Since then people have explored different ideas for reuse. Self’s delegative inheritance was a very interesting take, although it was still impossible to reason about dispatch in it. And, having implemented a Self dialect with multiple delegation slots, I found it quite painful to program as well.

At some point people have started saying “you should prefer composition over inheritance”. Mixins became a thing. And then Traits were developed to formalise mixins and resolve some of the fragility problems they had. Sadly, the only mainstream language with (at least partial) support for Traits today are PHP and Perl. Most languages support some form of Mixin though.

A Trait is basically a parameterised unit of composition/reuse. You define your reusable piece, and determine what people need to provide in order to make it work. For example, Equality can be defined as:

trait Equality<T> {
  requires compare(that: T): Ordering;
  define == (right: T) = left.compare(right) == Equals;
  define != (right: T) = !(left == right);
}

Any class/object may then adopt the Equality trait and provide the missing pieces. Equality will never look at any name besides the ones within its scope, and it’s not possible for any name to conflict with existing ones in the adopting object/class:

class Id<T>(value: T where T is Comparable) {
  // Equality doesn't see this, because it's not in its scope
  define compare(that: Id<T>) = value.compare(that.value);
  
  include Equality<T> {
    // this here is `Id`, not `Equality`
    provide compare(that) = this.compare(that);
  }
}

If Id already defined a method == or !=, it’d have to explicitly tell the compiler what to do with it, with the options being not using it, or renaming it:

class X() {
  define != (right: T) = ...;
  include Equality<T> {
    provide compare(that) = ...;
    // not doing this would be a compile-time error
    rename != to notEquals;
  }
}
X().notEquals(X()); // Equality's !=
X() != X(); // X's !=

“Private variables are not enough”

(to be honest I don’t quite get what Charles was trying to say for this point)

Anyway. People usually think that encapsulation is about protecting your data from evil outsiders. And… well, sure, that’s kind-of part of it. But that isn’t the whole story.

First, let’s talk about what’s encapsulation. You’ll see all sorts of definitions out there, but here we’ll just think of it as “a mechanism for controlling access to some set of things.”

Encapsulation is a very important thing in modules and modularity. And by modules I mean objects (or at least Rossberg et al’s formulation of it in F-ing modules). A module has a public interface, which can be used by outsiders, and a set of internals, which only concerns its implementation.

There’s nothing else that encapsulation grants you, but this notion of access control is not fine-grained enough for most needs, if you ask me. Object-Capability Security extends this notion to fine-grained access control. And objects are a really nice way of expressing this, as well as granting more privileges.

The one thing that encapsulation does not give you, however, is control over mutability. It doesn’t even try to do that. Hardly any programming language in existence tries, to be honest. With linear types, and reference capabilities, however, we can guarantee these things. Rust and Pony are probably the only programming languages that have something of the kind right now.

“Polymorphism isn’t exclusive to OOP”

This is one of the points that bother me, because polymorphism is a type system thing. It’s about making your compiler accept many different shapes in a single code path. Most OOPLs have subtyping polymorphism or some kind of ad-hoc polymorphism, some have parametric polymorphism, and there generally isn’t much besides that.

The reason it bothers me is this paragraph in Kay’s accounting of the history of Smalltalk, where he described the idea of messages in Smalltalk-72:

“This led to a style of finding generic behaviors for message symbols. “Polymorphism” is the official term (I believe derived from Strachey), but it is not really apt as its original meaning applied only to functions that could take more than one type of argument.” — Alan Kay

So, Strachey’s idea of polymorphism is about types. But Kay’s idea of “generic behaviours” is not about that — it’s about behaviours. The idea that in “3 + 3” and “[1] + [2]”, it’s the operands of that expression that define what it means, not the operator.

While functional languages have historically favoured parametric polymorphism, as Strachey argues for in his paper, they have recently started supporting some forms of ad-hoc polymorphism as well: TypeClasses in Haskell, Protocols in Clojure, multi-methods in Common Lisp, etc. With the exception of TypeClasses and some ML modules (because they’re not first-class), you could view all of these as objects as well. The mechanisms for choosing operations dynamically is pretty similar.

Conclusion

Object Oriented Programming has many problems. And, of course, Functional Programming has its own share of problems. But it’s still important to understand that these paradigms are not a single set of concepts that’s set in stone — sure they’re a collection of concepts, but these change over time.

OOP and FP regularly steal from each other in order to solve the problems they have, and both improve as a result. That’s how we got better module systems in FP. Heck, “stealing from FP” was how we got OOP in the first place!

I just hope people’ll some day stop seeing programming paradigms as some monolithic set of features, and more as a simple core calculus with a selection of non-paradigm-specific concepts on top (which you, of course, can use elsewhere).

Additional Reading

References

William Cook (2012)— A Proposal for Simplified, Modern Definitions of “Object” and “Object Oriented”

Craig Chambers, David Ungar, Bay-Wei Chang, and Urs Hölzle (1991) Parents are Shared Parts: Inheritance and Encapsulation in Self

Alan Kay (1993) — The Early History of Smalltalk

Andreas Rossberg, Claudio Russo, and Derek Dreyer (2010) — F-ing Modules

Mark S. Miller (2006) — Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control

Christopher Strachey (2000) — Fundamental Concepts in Programming Languages

Luca Cardelli (1996) — Type Systems

Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz, and Andrew P. Black(2002) — Traits: Composable Units of Behavior

Pony’s Reference Capabilities (from the Pony tutorial)

Rust’s Ownership System (from the Rust book)