apropos

Multiple Inheritance with Zero Boilerplate

Object-oriented programming is a programming paradigm found in languages notorious for verbose syntax and boilerplate code. Yet it also provides for patterns that, while often considered to be opaque and bad practice, are also useful: like multiple inheritance. Multiple inheritance is the idea that an object can inherit the properties of multiple different objects: and so be usable as a replacement for any of those. This takes many different forms: from full-fledged wretched multiple inheritance of full classes, to multiple inheritance being allowed only via interfaces, to multiple inheritance not being allowed at all and instead manually done via casting.

In a course on type systems I took this past term, we had a reading on subtyping: the idea that one type can be safely used in the context of another type, given some conditions hold. These conditions differ between structural type systems and nominal type systems. In a structural type system, the names, types, and occasionally order of complicated types (structs/unions, mostly) are all that matters when checking if a subtyping relation is valid. While in a nominal type system, they similarly must obey structural subtyping rules, but the subtype relation must be explicitly denoted (typically through use of an “extends” or “inherits” or similar keyword).

Having subtyping rules on a structural type system, alone, then, is enough to facilitate a minimal working system of multiple inheritance. And these subtyping rules are dead simple! I found this idea really quite interesting: it seemed to be obvious to some other people in the class having more extended experience with object-oriented programming languages (Java, Python, C++), but most of what I knew going in about language design came from the imperative / functionalish Nim, which lacks multiple inheritance or even a class keyword. And so it was entirely new and interesting!

Such a system could be built on top of either a structural or a nominal system, since nominal systems extend structural systems, somewhat. I’ll not be exploring the nominal side of this here: as it adds somewhat of the boilerplate this post is focused on getting rid of, though the reader might find it interesting to think about what kind of minimum viable syntax could keep the explicitness guarantees of typical inheritance systems.

Structural Typing

Let’s first revisit what it means to have a structural type system. Such a system is a type system where type equality only relies on the names (sometimes), types, and positions (sometimes) of fields in a complicated type. There are two such types we’ll deal with here: structs and unions.

structural structs

Consider the following types:

type Foo = struct
  x: int, y: int

type Bar = struct
  x: int, y: int, z: int

type Baz = struct
  y: int, z: int

Bar has all of the fields of Foo. In our structural type system, we may consider Bar to be a subtype of Foo, as it has a superset of its fields. This terminology is confusing! Bar being a subtype of Foo implies it is somehow less than Foo: but it has more fields! But this subtype relationship comes from that Bar can substitute for Foo wherever Foo is expected:

func display_xy_coords(self: Foo): str =
  return "{}, {}".fmt(self.x, self.y)

let foo: Foo = { x: 1, y: 2 }
let bar: Bar = { x: 1, y: 2, z: 3 }
let baz: Baz = { y: 2, z: 3 }

print display_xy_coords(foo) # 1, 2
print display_xy_coords(bar) # 1, 2
print display_xy_coords(baz) # type error

structural unions

Now consider these union types:

type Foo[T] = union
  str, list[T], array[T]

type Bar[T] = union
  list[T], array[T]

type Baz[T] = union
  str, list[T], array[T], int

Foo has all the variants of Bar. In our structural type system, we may consider Bar to be a subtype of Foo, as Bar can substitute for Foo wherever Foo is expected. Foo can be a string, list, or array. Bar can only be a list or an array. Thus, Bar is safe to use where Foo is required.

Note that this behavior is opposite the behavior of structs above! These structs and unions, as we have described them, are actually formally dual: further reading on this front can be found by looking for “product types” (structs) and “sum types” (coproduct types, unions).

func get_length[T](self: Foo[T]): int =
  return self.len

let foo: Foo = "hello, world!"
let bar: Bar = ['h','e','l','l','o',' ','w','o','r','l','d','!']
let baz: Baz = 1025

print get_length(foo) # 13
print get_length(bar) # 13
print get_length(baz) # type error

Multiple Inheritance

Let’s tie it all back together, and provide some nice, classic, totally-not-contrived-and-awful-pedagogically object-oriented examples.

Consider a Building. A Building has an address. We could extend this building type to, say, an Office that additionally has a list of Employees, a House that additionally has a list of Mail, and a PostOffice that additionally has both a list of Mail and a list of Employees. (again, a contrived example.)

In Java, this presents a problem! We simply must consider a PostOffice both a House and an Office to appease the object-oriented gods. Yet there is no way to do such with classes alone: interfaces must be used, which present a much nicer structure and do not run into the deadly diamond of death. But that is beside the point: you can do multiple inheritance in Java and other languages, but it’s verbose and ugly.

In our example structural language, multiple inheritance is supported with no more than a passing thought.

type Building = struct
  address: str

type Office = struct
  address: str
  employees: list[Employees]

type House = struct
  address: str
  mail: list[Mail]

type PostOffice
  address: str
  employees: list[Employees]
  mail: list[Mail]
func empty_mail(self: mut House): list[Mail] =
  let mail = self.mail
  self.mail = []
  return mail

func get_employees(self: Office): list[Employees] =
  return self.employees

let foo: Office = { address: "123 Abigail Lane", employees: ["Alice", "Bob"] }
let bar: House = { address: "456 Bartleby Lane", mail: mail }
let baz: PostOffice = { address: "456 Bartleby Lane", employees: ["Craig", "Dan", "Erin"], mail: mail }

print get_employees(foo) # "Alice", "Bob"
print get_employees(baz) # "Craig", "Dan", "Erin"
print get_employees(bar) # type error

Conclusion

This is probably pretty obvious to anyone familiar with structural typing, or extremely familiar with object-oriented languages! But I did not fall into either of those camps, and so found it pretty neat. Perhaps because it was my first language, and I always implicitly compare everything new to it, but I find it fascinating how vast amounts of Java’s boilerplate and the arbitrary restrictions surrounding it (ex. can’t re-declare fields with a different type in extended classes) can be brushed away by simpler, more powerful programming paradigms.

Of course, such a system implemented in a real language wouldn’t be to everybody’s taste. By intention this subtyping is implicit, and if somebody down the line wanted to write really bad code taking advantage of this, there wouldn’t be any sticky webs of explicitness to stop them. In addition, if you allow function overloading, you have to contend with the deadly diamond of death: and so perhaps a better route is to only allow such structural ambiguity explicitly by way of interfaces. In general, however, I think that eschewing boilerplate and restrictiveness in favor of semantics by structure is a good thing, and so I’d like to see more languages explore more structural systems.


Further reading: Types and Programming Languages, primarily the sections on subtyping (Chapter 15) and Featherweight Java (Chapter 19)