Let’s talk about inheritance. Now, let me be clear here: when I say “inheritance”, I’m not specifically talking about OO inheritance. I’m talking about a facility, built into the language, for saying that one thing satisfies the interface of another type of thing, and has additional functionality on top of that. Most modern languages have something that’s sort of like this. Object-oriented languages have inheritance (either through the class hierarchy, interfaces, or mixins, or whatever), Haskell has superclasses, and so on. You already know about inheritance and agree that it, in one of its many forms, is useful. But this post isn’t about that.
This post is about reverse inheritance. As far as I know, no popular language supports reverse inheritance conveniently. (I’ve been wrong before — if it turns out I’m wrong, and some popular language does have this feature, please do point it out to me.)
What do I say when I mean “reverse inheritance”? Reverse inheritance is the opposite of inheritance. If one datatype A inherits from another datatype B, it means that A supports all the functionality of some B, and then possibly some more functionality. If one datatype A reverse-inherits from another datatype B, it means that A supports some of the functionality of B, but not necessarily all of it. Here’s an example in a made-up language:
fn frobulate(s: Socket)
In this example,
s is a
Socket and so it satisfies the
Socket interface; i.e., it can do everything a
Socket can do. There are a few things you can do with a socket: you can connect it to an address, or you can read from it or write to it, or you can close it. Since
s can do everything a socket can do, the function
frobulate can accordingly can do any of those things to the socket.
Now let’s try the same thing, but with reverse inheritance:
fn frobulate(s: Socket(read))
s isn’t a
Socket, or at least not necessarily.
s reverse inherits from
Socket, and the method
read is reverse-inherited.
s supports the
read method of the
Socket, but no other method.
Now, in the strongly- and statically-typed hypothetical language we’re talking about here, reverse-inheritance is checked at compile time, so it’s a compile error if any method other than
read is called on
I don’t get it.
So, unlike traditional inheritance, it doesn’t add functionality to datatypes. It actively takes away functionality from datatypes. Since they’re opposites, the goals they accomplish are completely orthogonal. Inheritance tends to aid code reuse and abstraction, while reverse inheritance aids code readability and facilitates making static, verifiable guarantees about program behavior — which, maybe coincidentally, are not things that traditional inheritance tends to be good at.
Here’s what I mean. You see this type signature:
fn frobulate(s: Socket(read))
… And you know that no method besides
read is called on
s in that function. The socket doesn’t get written to, it doesn’t get closed, it doesn’t get bound to an address: it just gets read, and that’s it. This is very good for code readability: if a function makes good use of reverse-inheritance, then you don’t have to pore through a function’s source code, and all of the functions that function calls, in order to understand what concrete side effects a function has. Of course, this doesn’t just work for sockets. You can reverse inherit from a database driver to ensure at the type level that the connection is never closed, or that only SELECT queries are sent (in other words, you can guarantee that the method never changes anything in the database). Imagine taking your MVC app and using reverse-inheritance to statically ensure that your program is structured correctly, because you can ensure the model, view, and controller only call methods that they each should be calling. All of this also works to make sure you’re not changing the semantics of your program when you refactor, too.
So does this mean I would have to annotate the parameters of every function I write with all the methods that function calls?
Not necessarily. If your function makes use of the entire interface of a datatype, or almost its entire interface, then it’s probably not worth it to specify exactly which behaviors are used. If you’re unsure whether the implementation of a function will change, then you might want not want to reverse-inherit just in case you end up calling other methods on those parameters in the future. The question you should be asking is: “does it make sense to guarantee that this function will only ever use a small subset of the behaviors provided by an interface?” Does it make sense to limit the scope of what a function can do at the type level? If so, then reverse inheritance could be an elegant way to capture that requirement.
Of course, it doesn’t matter that much, because it’s a hypothetical language feature. OO languages only support inheritance going one way, and there’s no real clean way to say “this object supports methods X, Y, and Z of this other object, but none of the others”. Haskell, in spite of its supreme type system, also does not make this easy. In both Java and Haskell, if you have 10 operations on a datatype A, but you want the type system to guarantee you only made use of one particular operation, you have to define a new wrapper datatype around A that only supports that operation — pretty messy, and not very convenient or obvious. Reverse inheritance is more direct and immediate.
Here’s the funny part: of all the statically typed languages I know, none support true first-class reverse inheritance, but one makes it markedly easier than all others. What is this language and its type system of kings?
Believe it or not, it’s Go. In Go, structs don’t have their interfaces declared, as they’re dynamically inferred according to whatever methods end up being defined on the struct. So if you define an interface I which happens to “reverse-inherit” from another interface J, then any type which implements J automatically implements I. This is not generally the case for other languages, which often require you to specify all of the interfaces a datatype satisfies exhaustively. So Go happens to provide a better reverse-inheritance story than all of the other statically typed languages I know, which is pretty neat. That makes it easier for them to reap the rewards I detailed above — increased readability and ability to statically reason about code behavior and correctness — but given the kind of people that tend to be drawn to a language like Go, I’m sure they’re not using this accidental feature for that purpose very much.