A solid grasp of the tools Ruby has for inheritance helps with
writing better code.Especially Ruby code typed with Sorbet where
inheritance underlies things like abstract methods, interfaces, and
generic types.
On the other hand, when most people learn Ruby they learn
just enough of what include
and extend
mean to
get their job done (sometimes even less 🫣).
I’d like to walk through some examples of inheritance in Ruby and draw little diagrams to drive their meaning home. The goal is to have inheritance in Ruby “click.”
The <
operator
Before we can get to what include
and
extend
do, let’s start with classes and superclasses.
class Parent
def on_parent; end
end
class Child < Parent
end
Child.new.on_parent
This is as simple as it gets. Most languages use the
extends
keyword to inherit from a class. Ruby cutely uses
the <
token, but otherwise it’s very straightforward.
When we call on_parent
on an instance of
Child
, Ruby finds the right method to call by walking the
inheritance hierarchy up to where that method is defined on
Parent
.
I picture Ruby’s <
operator as working something like
this:
Nerd alert: these diagrams will be ignoring multiple
inheritance. To be more accurate we’d have to draw them showing that
classes can have one parent class and multiple parent modules. But Ruby
linearizes the hierarchy, so this conceptual model of “follow a single
chain up” will be good enough. Multiple inheritance can be a future
post.
In particular, I picture classes like puzzle pieces. The pieces have
tabs and blanks“Tabs” and “blanks” are the names
Wikipedia uses for these spots on jigsaw puzzles.
which allow other classes to slot in, forming an
inheritance hierarchy.
Checking whether a class is a subclass of another amounts to
following the chain upwards. If you can reach ClassB
from
ClassA
, then a.is_a?(ClassB)
.
Method dispatch does the same up-the-chain search, stopping in each
class to look for a method with the given name. Does Child
have a method named on_parent
? Nope, so let’s go up and
keep checking. Does Parent
? Yep—let’s dispatch to that
definition.
Here’s the first wrench Ruby throws into the inheritance
mix: the <
operator not only sets up a
relationship between the classes themselves, it also
makes the singleton class of Child
inherit from the
singleton class of Parent
.
I’ll show what I mean in code first:
class Parent
def self.on_parent; end
end
class Child < Parent
end
Parent.new.on_parent # ❌
Parent.on_parent # ✅
Child.on_parent # ✅
Now the on_parent
method is on the singleton class
because of the def self.
(compared with just
def
before). Since it’s defined on the singleton class,
it’s only possible to call it on the class object itself, not on
instances of the class. And more than that, it’s available on the
singleton class of Child
, because the <
operator also set up an inheritance relationship on the
singleton classes.
Which means we need a slightly more involved picture to show what the
<
operator is doing, which I’ll represent as this
red/blue jigsaw piece:
The <
operator takes a normal class and a singleton
class and links them up with another normal and singleton class, so we
get two inheritance relationships for the price of one <
token in our code.
We’re going to work our way up to a full toolbox of these inheritance jigsaw pieces. As a sneak preview:
Don’t worry if that doesn’t click yet, we’ll get there. But first, a
detour about why we even want the <
operator to work
like this in the first place.
Wait, why do we care about inheriting both?
Ruby has a rich link between a class and its singleton class. The
<
operator just preserves this link across inherited
classes. Let’s unpack these observations.
In Ruby, singleton classes are first-class objects. You can pass them around and call methods on them just like any other object:
class A; end
foo(A)puts(A.name)
Not only are they first class, but it’s seamless to reflect from an instance of a class up to the object’s class:
.class # => A a
To take it a step further: objects in Ruby are instantiated by
calling new
,In Ruby new
is not a keyword, it’s a
normal method! (It’s unlike the new
keyword in C++ or
Java.)
a singleton class method:
class A; end
class B; end
def instantiate_me(klass)
# instantiation is dynamic dispatch:
.new
klassend
instantiate_me(A) instantiate_me(B)
These two methods form an intrinsic link between a class and its
singleton class. They power all sorts of neat code in the wild, too. For
example Sorbet’s
T::Enum
class looks something like this under the
hood:
This TypedEnum
class is a simplification
of Sorbet’s T::Enum
, which is more robust. But the full
implementation fits in a single
file if you’re curious.
class TypedEnum
def self.values = @values ||= {}
def self.make_value(string_name)
return v if (v = values[string_name])
self.new(string_name)
end
private_class_method :new, :make_value
def to_s = @string_name
def initialize(string_name)
@string_name = string_name
self.class.values[string_name] = self
end
end
This TypedEnum
class implements the typesafe enum
patternPopularized by Joshua Block in Effective Java, First
Edition, Item 21, in response to the observation that much Java code
would use magic integers to represent enumerations. (The same thing
happens in Ruby, but with magic Symbols and Strings in addition to just
Integers.)
, which is a way of guaranteeing that there are only a
fixed set of instances of a class, which can only be compared to values
of the same enum (not unrelated enums).
You’d define an enum using this abstraction something like this:
class Suit < TypedEnum
CLUBS = make_value("clubs")
DIAMONDS = make_value("diamonds")
HEARTS = make_value("hearts")
SPADES = make_value("spades")
end
It’s so concise in Ruby“Concise” versus the original Java pattern. Sorbet’s
T::Enum
makes the pattern even more concise.
because of the special relationship between a class and
its singleton class:
First, the implementation of initialize
can reflect back
up to the class with self.class.values
to share information
across all instances of a class.
Second, the singleton class method make_value
calls
self.new
, which uses dynamic dispatch to
instantiate an instance of whatever class make_value
was
called on (like Suit
above). This dynamic dispatch only
works because the <
operator set up an inheritance
relationship on the singleton class, too!
Third, the TypedEnum
class encapsulates all of the logic
for what it means to be a typesafe enum. The Suit
class has
no implementation of its own, relying entirely on method resolution up
the inheritance chain.
To recap: the cool part about attached classes and singleton classes
and inheritance in Ruby is that there’s this link between instance and
singleton, via self.class
and self.new
.
This link is preserved by having the <
operator also create an inheritance relationship between singleton
classes.
Aside:
self.new
and self.class
in the type
system
My main focus here is to show how Ruby models inheritance, but now’s a perfect time to sneak in a note about how Sorbet works.
Because this self.new
/self.class
link is so
special, Sorbet captures it in the type level, as well:
sig do
params(n: String).returns(T.attached_class)
end
def self.make_value(n)
self.new(n)
# ^^^^^^^^^^^ T.attached_class (of TypedEnum)
end
sig { params(n: String).void }
def initialize(n)
self.class
# ^^^^^^^^^^ T.class_of(TypedEnum)
end
If you call self.new
inside of a singleton class method,
the type that you get back is what’s called
T.attached_class
. It’s a weird name. People who use Ruby
are very familiar with having the singleton class called the singleton
class. They usually don’t have a name for this other class, but
it’s called the attached class: it’s the name that the Ruby VM uses, and
it’s also the name that Sorbet uses.
Here’s how to think about what this type means: it is the
type in Sorbet that exists to model what new
does. It
models the linkage from a singleton class back down to its attached
class. And it respects dynamic dispatch:
class Parent
{ returns(T.attached_class) }
sig def self.make; self.new; end
end
class Child < Parent; end
= Parent.make # => Parent
parent = Child.make # => Child child
There’s only one definition of the make
method, but the
two calls to make
above have different types. Sorbet knows
that if make
is called on Parent
, the
expression will have type Parent
, and if called on
Child
will have type Child
. That’s the power
of T.attached_class
, and it’s precisely the type that
captures how new
works.
In the opposite direction, T.class_of(...)
is the type
that represents following the link from the attached class up to the
singleton class by way of self.class
. It says, “Whatever
class you are currently in, if you call self.class
you will
get
T.class_of(<whatever class you are currently in>)
.”
For our initialize
method defined in the
TypedEnum
class, self.class
has type
T.class_of(TypedEnum)
. It’s the name Sorbet uses for the
singleton class—the Ruby VM would use the name
#<Class:TypedEnum>
, instead. Sorbet and the Ruby VM
represent the concept of a singleton class with different names.
For more on these types, check out the Sorbet docs:
→ T.class_of
→ T.attached_class
But now let’s get back to inheritance in Ruby.
The include
operator
So far we’ve only been talking about classes. Ruby also has modules, which are kind of weird.
module IParent
def foo; end
def self.bar; end
end
class Child
include IParent
end
Child.new.foo # ✅
Child.bar # ❌
#
If we think of classes as both “a grouping of methods” and “the ability to make instances of that class,” modules are only the ability to group methods. You can’t instantiate a module: you can only use them to make this little namespace for methods.
But we can use those modules in inheritance chains. The
instance method foo
in our example above can be called on
instances of Child
because of how include
works. But importantly: the singleton class method bar
cannot be called on the class object
Child
, unlike with the <
operator.
In picture form, include
is a puzzle piece that only
links up module instance methods with the child class:
There’s something shocking here: not only do we not have a
puzzle piece that links up the singleton class of the module into the
singleton class of the child, there isn’t even a tab on
T.class_of(IParent)
. It’s smooth on the bottom. It is
not actually possible for a module’s singleton class to be
inherited. If we wanted to put a term to what’s happening here: module
singleton classes are final. They cannot be
inherited.
That comes with some interesting consequences.
It means a module’s members are never inherited, so if you have this
self.bar
method, you are never going to be able to call it other than by calling it directly likeIParent.bar
.I say “members” because it’s not just the methods: classes can have other kinds of members, most notably generic types, which I’ll revisit in a future post. But importantly, those members are never inherited. Module singleton classes are final.
It also has consequences for subtyping. When we look at the type
T.class_of(IParent)
which represents the singleton class ofIParent
, there is no type that is a subtype of that type. For example,T.class_of(Child)
is a singleton class, but it is not a subtype ofT.class_of(IParent)
. In code:You don’t even need to use Sorbet to show this point; you can observe the same thing by evaluating
p(Child < IParent)
and see that it’s false.
{ params(klass: T.class_of(IParent)).void } sig def foo(klass); end Child) # ❌ foo(
You cannot call
foo(Child)
because theChild
object has typeT.class_of(Child)
which is not a subtype of the parameter’s typeT.class_of(IParent)
.And finally, having no extension point breaks the link between
self.new
andself.class
.With a class’s singleton and attached class pair, there would be a link via
self.class
andself.new
. Modules do not have that—the singleton class is just, like, floating out in la la land. If a module instance method callsself.class
, it’s not clear which class that resolves to. If a module singleton method callsself.new
, that will raise aNoMethodError
exception at runtime.
Sometimes these limitations are fine: for example this does not
matter for the
Enumerable
module in the Ruby standard library, which
deals entirely with instance methods. But sometimes they’re not
fine.
The extend
operator
We might think, “Okay, well maybe this is just what Ruby’s extend is
meant to fix! Maybe extend
is the thing that preserves that
link.”
But no, even when using extend
there’s no way to get at
the methods defined on the module’s singleton class.
module IParent
def foo; end
def self.bar; end
end
class Child
extend IParent
end
Child.new.foo # ❌
Child.foo # ✅
Child.bar # ❌ (still)
extend
does something else, which is: if you
extend
a module, it still takes the instance methods
(because that’s the only extension point there is on modules), but it
slots them into the child class’s singleton class:
In picture form, there’s this weird half-red, half-blue puzzle piece
that makes it so that IParent
is an ancestor of
T.class_of(Child)
instead of being an ancestor of
Child
. It exposes instance methods on the module as
singleton class methods on Child
.
What it definitely doesn’t do is inherit the module’s singleton class, because module singleton classes are final.
So that basically wraps up inheritance in Ruby:
With classes, we have this puzzle piece which takes instance methods to instance methods and singleton class methods to singleton class methods. It’s cool because it preserves that link between instance and singleton.
But with modules, that link breaks down and the
onlyYou could argue these aren’t the “only” tools because
there’s also prepend
, but it doesn’t act different from
include
with respect to this link.
tools that we really have are include
and
extend
, which only affect instance methods in the
module.
Wait, why do we care if modules don’t work like classes?
It matters because sometimes a class already has a superclass. For
example, every Ruby struct descends from the Struct
class,
every activerecord
model in Rails descends from the
ActiveRecord::Base
class, etc. Sometimes we want to make
reusable units of code that slot into any class, comprised of both
instance and singleton class methods, that link up using
self.new
and self.class
.
So what are we to do? What if we need a mixin that wants to mix in both instance and singleton class methods?
Well, one option is “just use two modules.” This is gross, but it works:
module IParent
def foo; end
end
module IParentClass
def bar; end
end
class Child
include IParent
extend IParentClass
end
Child.new.foo # ✅
Child.bar # ✅
By convention, we could say that IParent
contains all
the instance methods, and that IParentClass
contains all
the methods that are meant to be singleton class methods, and make sure
by convention that IParentClass
is extended
wherever IParent
is included. So anyone who wants to use
this IParent
abstraction has to be sure to always mention
two class names, one with include
and one with
extend
.
That works—that makes both of these methods available, where
foo
is an instance method and bar
is a
singleton class method on Child
.
If we look at the puzzle pieces again, include is doing one thing to one module, extend is doing something else to some other module, and if we squint it kind of looks like our class inheritance puzzle piece?
But we still have two modules, and they’re kind of just floating
apart, unconnected to each other. It’s clunky. It was nicer with
<
, where we just had a single puzzle piece that linked
the attached and singleton classes.
The
mixes_in_class_methods
annotation
As it turns out, Ruby allows changing what
include
means.
I’ve already written about one tool which changes the meaning of
include
:
→ ActiveSupport’s
Concern
, in pictures
If you don’t use Sorbet, you probably just want to skip the rest of this post, and continue reading that one instead.
For historical reasons that might make it into another post, Sorbet
invents its own mechanism to achieve a result similar to
ActiveSupport::Concern
which it calls mixes_in_class_methods
.
The basic idea is to codify the “include
+
extend
” convention from above:
module IParent
extend T::Helpers
def foo; end
module ClassMethods
def bar; end
end
mixes_in_class_methods(ClassMethods)
end
class Child
include IParent
end
Sorbet provides this mixes_in_class_methods
annotation,
and using it in a module changes the meaning of
include
for the module with the annotation. The new meaning
of include
is twofold:
The original
include
still happens like normal, so whenChild
hasinclude IParent
it will still inherit fromIParent
.But then also: the
include
will find the associatedClassMethods
module and act as though that module wasextend
’ed on line 12. SoT.class_of(Child)
will descend fromIParent::ClassMethods
.
In a picture:
We get this really wacky-shaped puzzle piece, where our two modules
are still kind of unrelated to each other, but they’re at least
closer together. It acts like an include
and
extend
in one, but people don’t have to mention the
extend
.
Where
mixes_in_class_methods
falls short
So far so good except… it doesn’t quite work the way you’d hope it
might. The mixes_in_class_methods
annotation is kind of
dumb: it doesn’t pay attention to whether the include
happens into a class, or into another module. And if it happens into
another module, it will still act like the extend
was
written right there:
module IParent
extend T::Helpers
def foo; end
module ClassMethods
def bar; end
end
mixes_in_class_methods(ClassMethods)
end
module IChild; include IParent; end
# ^^^^^^
class Grandchild; include IChild; end
The implicit extend
happens on line 11, because
IParent
is the only class that has the
mixes_in_class_methods
annotation, and that’s where
IParent
is included.
But that means that T.class_of(Grandchild)
doesn’t have
IParent::ClassMethods
as an ancestor, because
IChild
is a module, and module singleton classes are
final:
T.class_of(IChild)
is a module’s singleton class, which
means it’s final, and will never be an ancestor of anything.
This is the biggest sharp edge to be aware of about
mixes_in_class_methods
—modules defined this way can’t have
dependencies on each other. It’s annoying.
When this comes up, one way to fix it is to just not mention
mixes_in_class_methods
in the upstream dependencies,
falling back to the explicit “include
+
extend
” convention from before
module IParent
def foo; end
module ClassMethods
def bar; end
end
# NO mixes_in_class_methods here
end
module IChild
extend T::Helpers
include IParent
module ClassMethods
# IParent doesn't have mixes_in_class_methods.
# Need to manually include here.
include IParent::ClassMethods
end
mixes_in_class_methods(IChild)
end
class Grandchild
# Can still depend on `IChild` in the convenient
# way, because it's at the bottom of the stack
include IChild
end
class Child
# IParent doesn't have mixes_in_class_methods.
# Need to manually extend here.
include IParent
extend IParent::ClassMethods
end
The IParent
module is upstream of the
IChild
module, so IParent
doesn’t use
mixes_in_class_methods
. Meanwhile IChild
is
not upstream of any other modules, so it’s free to use
mixes_in_class_methods
like before.
Since IParent
does not have
mixes_in_class_methods
, we have to fall back to our
“convention-only” approach before. We see this on lines 17 and 32, where
the IParent::ClassMethods
have to be brought in
manually.
But having done that, at least T.class_of(Child)
now has
the ancestor chain we were looking for, where
IParent::ClassMethods
is an ancestor:
I should say: I consider this to be a wart in Sorbet’s design.It’s a long-term goal of mine to fix this one day,
either by implementing support for Concern
in Sorbet or
even replacing mixes_in_class_methods
with
Concern
.
When we look at how
ActiveSupport::Concern
works, it’ more like what you’d
expect: it’s a bit more recursive or viral about linking up the
ClassMethods
classes when stacking modules on top of
modules. Hopefully simply being aware of this sharp edge in
mixes_in_class_methods
is enough for now.
Inheritance in Ruby
Some things we learned in this post:
It’s cool that classes are first-class objects in Ruby.
Being first-class means that it’s easy to follow the link from a singleton class down to the attached class (with
self.new
) and back up (withself.class
).Ruby’s
<
operator for inheriting classes preserves this link, by making a class’s singleton class descend from its parent’s singleton class.That link breaks down for modules, because module singleton classes are final. No amount of
include
norextend
change that fact.It’s possible to use modules to approximate class inheritance in Ruby, using
ClassMethods
modules either by convention or with things likemixes_in_class_methods
.Sorbet’s
mixes_in_class_methods
isn’t as smart as Rails’ActiveSupport::Concern
when it comes to mixing modules into other modules (but maybe one day will be).
It was only after internalizing these concepts that I started feeling in control when working in Ruby codebases. Hopefully seeing things laid out this way makes you feel more in control as well.
Appendix: Further reading
Some links that I think are pretty interesting and relate to the topics covered in this post:
Dynamic vs. Static Dispatch, by Lukas Atkinson
A discussion of what we mean when we say dynamic dispatch or static dispatch, with a particular focus on C++, where even instance methods use static dispatch by default unless you explicitly use the
virtual
keyword.Interface Dispatch, also by Lukas Atkinson
A follow-up post discussing how multiple inheritance can be implemented under the covers, discussing implementation considerations in C++, Java, C#, Go, and Rust, and the tradeoffs that each makes in service of the flavor of multiple inheritance each chooses to support.
Versioning, Virtual, and Override: A Conversation with Anders Hejlsberg, Part IV, interview with Bruce Eckle and Bill Venners
“Anders Hejlsberg, the lead C# architect, talks about why C# instance methods are non-virtual by default and why programmers must explicitly indicate an override.”
Why doesn’t Java allow overriding of static methods?, Stack Overflow answer by ewernli
Classes do not exist as objects in Java. With
myObject.getClass()
you get only a “description” of the class, not the class itself. The difference is subtle.Dynamic Productivity with Ruby: A Conversation with Yukihiro Matsumoto, Part II, another interview with Bill Venners
“Yukihiro Matsumoto, the creator of the Ruby programming language, talks about morphing interfaces, using mix-ins, and the productivity benefits of being concise in Ruby.”
Setting Multiple Inheritance Straight, by Michele Simionato
“I have argued many times that multiple inheritance is bad. Is it possible to set it straight without loosing too much expressive power? My strait module is a proof of concept that it is indeed possible. Read and wonder …”