Pure Objects — a Vision For the Next Generation of Object Oriented Programming Languages
If you’ve studied object-oriented programming, you’ve likely come across the “three principles of object-oriented programming”. These are said to be:
- Encapsulation
- Inheritance
- Polymorphism
You might also have heard that the definition of an object is a unit of computation, which contains data, but only exposes mechanisms for mutating that data through accepting messages from other objects, invoking methods on the object.
But then you might have been introduced to concepts like “Composition over Inheritance”. Or told about the “Monkey/Banana Problem”, or shown the “Fragile Base Class”, or the “Diamond Problem”.
So when someone — a coworker, a friend, or a “thought leader” — told you that functional programming is “better” than OOP, you were ripe for convincing.
The functional paradigm promised better reasoning, better reusability, improved error handling, easy(!) concurrency. Indeed, functional programming languages have a long list of utilities that supposedly makes OOP “obsolete”.
In my mind, most — if not all — of these advantages stem from the functional languages’ implementation of pure immutability. And it seems like this property is diametrically opposed to the concept of an object. Because objects are supposed to contain mutable state, right?
No, I don’t think so. Objects are supposed to represent the states of things in the natural world, but that definition says nothing about mutability. I reached this conclusion after reading this Quora answer from Alan Kay (famously the “father of OOP”). In the post, Alan writes the following:
[…] this would allow “real objects” to be world-lines of their stable states and they could get to their next stable state in a completely functional manner. In the Strachey sense, they would be “viewing themselves” with no race conditions to get their next version.
So I took another look at the dominating object-oriented languages, and realized it was completely possible to “translate” every concept into its immutable counterpart. Let’s take a look!
public class Account {
private int balance = 0; public void deposit(int amount) {
balance += amount;
} public void withdraw(int amount) {
balance -= amount;
} public int getBalance() {
return balance;
}
}
This would probably be the canonical example of encapsulation in OOP. Before looking at its immutable sibling, let’s remove some of the sugar that the language (Java in this example) gives us.
public class Account {
private int balance; public Account() {
this.balance = 0;
} public void deposit(int amount) {
this.balance = this.balance + amount;
} public void withdraw(int amount) {
this.balance = this.balance - amount;
} public int getBalance() {
return this.balance;
}
}
What becomes clear here is that the constructor receives an implicit argument called this, which looks sort of like a struct from lower level languages. The dollars instance variable is initialized to 0. The deposit and withdraw methods receive the same implicit argument, which it can both read from and write to.
A good OOP practitioner will tell you to separate the methods that return information about the object from the methods that change the object. This is referred to as “Command/Query Separation” or CQS. The void return type from the methods that change the state communicates that mutation. So even though you can mutate an object and return a value, it’s not good practice.
Let’s return to Kay’s essay: “[…] this would allow “real objects” to be world-lines of their stable states and they could get to their next stable state […]”. This suggests the implementation of immutable objects, of which I have written before. That would transform our Java class into this:
public class Account {
private final int balance; private Account(int balance) {
this.balance = balance;
} public Account() {
this(0);
} public Account deposit(int amount) {
return new Account(balance + amount);
} public Account withdraw(int amount) {
return new Account(balance - amount);
} public int getBalance() {
return balance;
}
}
The key here is that the methods that previously returned void (the “command” methods in CQS terminology) now return new instances of Account. Indeed, the objects “get to their next stable state” by “viewing themselves”.
This is not an entirely uncommon pattern, but it’s much more verbose than the original class. That’s because Java wasn’t designed around classes looking like this.
So what would it look like if there was a language that was designed around immutable objects? What if we took at as far as it can go? How would an object oriented language look if it was as religious about immutability as Haskell?
The WorldLines Programming Language
In honor of Kay, let’s create an imaginary language called WorldLines. Let’s walk through the implications of removing mutable state from a Java-like language.
1. The Type of This
Within a method of a Java class, the implicit this variable has the type of the class that we’re currently editing. It is guaranteed to be a covariant of the current class, meaning that if we’re editing the class Mammal, we know that this is an Animal, as well. It can, however, also be a Dog, if the method is inherited. We can check for and cast to that case, but we generally avoid doing so, in favor of polymorphism.
public class Animal {}public class Mammal extends Animal {
public void method() {
// `this` is known to be both an
// Animal and a Mammal, but
// might very well be a a Dog.
}
}public class Dog extends Mammal {}
When we change the state of the object, the type is stable. It doesn’t change. So a method that is known to change something in the Mammal class is known to change the same thing inside a Dog object, without the Dog changing type.
This is not the case for our immutable implementation in Java. A change to a Mammal object must return a new Mammal, and so our Dog stops being a Dog.
To fix this, we need to invent a way to represent the immutable change to an object without changing its identity. In functional languages, struct-like data known as records are changed by applying a “patch” to an existing instance of the record, retaining the non-patched fields’ values.
So we’ll invent a patch keyword! It works like this:
Mammal mammal = new Dog();
Mammal mammal2 = patch (mammal)
..accessibleField = "new value"
..otherField = 123;
assert(mammal2 instanceof Dog);
assert(mammal2.accessibleField == "new value");Let’s rewrite our Account class with this new construct:
public class Account {
private int balance = 0; public Account deposit(int amount) {
return patch (this)
..balance = balance + amount;
} public Account withdraw(int amount) {
return patch (this)
..balance = balance -amount;
}
...
}
Note that we can now remove our constructors and reclaim the initializer, since we have a way to create new instances without using a constructor. But we still have a problem. Consider the following:
public class PremiumAccount extends Account {}PremiumAccount account = new PremiumAccount();
PremiumAccount account2 = account.deposit(19);
The return type of deposit is still just Account, and would have to be downcast to PremiumAccount. Not optimal!
The patch expression returns an object with the same type as the one being patched. So how do we encode that the this’s type is returned? We’ll take inspiration from TypeScript here, and introduce a this type.
public class Account {
...
public this deposit(int amount) {
return patch (this)
..balance = balance + amount;
} public this withdraw(int amount) {
return patch (this)
..balance = balance -amount;
}
...
}
2. Scaling Up
In an environment where every class is written like this (since there is no alternative), if a method on one class forwards a message to a member, and so on, the composition of immutable objects seamlessly fit together:
public class A {
private int state = 0;
public this updateA() {
return patch (this)
..state = state + 1;
}
}public class B {
private A a = new A();
public this updateB() {
return patch (this)
..a = a.updateA();
}
}public class C {
private B b = new B();
public this updateC() {
return patch (this)
..b = b.updateB();
}
}C c = new C().updateC().updateC(); // c.b.a.state == 2
3. Collateral Improvement
These small changes propagate through the design of the language and the APIs that emerge from it. For instance, we’ve inadvertently made it impossible to violate the Law of Demeter, because if you did new C().getB().getA().updateA(), you would get a new A back, and the original A within the B within the C would be unchanged.
Another effect of this is that a class with no constructor is a constant object. Think about it; the only way to have meaningful differences between two new instances is if you send in different arguments into their constructors. If the constructor has no parameters, then this would be true:
assert(new X() == new X());Therefore, we might consider changing the behaviour of the constructor to be more like the initial patch:
public class ClassWithACompleteInitialState {
private int state = 10; public this doubleState() {
patch (this)
..state = state * 2;
}
}public class ClassWithAConstructor {
private int state; public ClassWithAConstructor(int state)
..state = state; public this doubleState() {
patch (this)
..state = state * 2;
}
}ClassWithACompleteInitialState; // A constant object
ClassWithAConstructor; // (int) -> ClassWithAConstructorClassWithACompleteInitialState.doubleState();
ClassWithAConstructor(20).doubleState();
4. Side Effects
So how would you perform side effects in this system? There’s this word that keep coming up in functional programming; monad. Most people think it’s very hard to understand what a monad is. All they know is that monads supposedly make side effects pure in functional languages.
I’m happy to tell you that you’ve probably already used a monadic object. For instance, in JavaScript, the Promise is a monadic object. In C#, the IEnumerable interface’s extension methods provided by Linq is implemented in a monadic way.
It all boils down to an object (or a function in FP) representing a changed/changing state even before the change has happened, allowing chaining of other operations to be appended to the same pure object.
public class Database {
public Promise<void> execute(String query) { ... }
}public Promise<void> setup(Database db) {
return db.execute("CREATE TABLE users ...")
.then(() ->
db.execute("INSERT INTO users ...")
);
}public Promise<void> main() {
return connectToDatabase("postgres://...").then(setup);
}
The then calls (or flatMap/map in more explicitly functional languages like Scala) can be syntactically sugared to reduce some of the noise. JavaScript, C#, Dart, and others have async/await. Haskell has do notation. Scala has for comprehensions. Choose your favorite!
Summary
Functional languages are very different from object oriented languages today. But I think that without changing the fundamental principles of OOP, we can still have a lot of the benefits that FP has, by fully embracing immutable objects.
We can keep our encapsulation, our subtype polymorphism (as opposed to parametric polymorphism common in FP), and even our inheritance, without leaving the world of immutability and referential transparency. Then we can adopt the laziness of languages like Haskell.
The conflict between functional languages and the principles of OOP only lies in code organization, and how polymorphism is achieved (subtype vs parametric). The rest is just syntax and preference.
I sure hope we’ll see competitive languages in this paradigm come out in the next few years.
