Let's take a step back: what is a mixin? Gilad Bracha didn't invent mixins (they were invented as a design pattern in the Flavors object-oriented extension to Lisp Machine Lisp), but he wrote the seminal paper on them (his PhD thesis "The Programming Language 'Jigsaw': Mixins, Modularity and Multiple Inheritance"), in which he defines what mixins are, and shows that mixin composition subsumes all other forms of classical inheritance. According to this paper, a mixin is a class parameterized by its superclass. So, you can think of a mixin as a function :: Class → Class
.
What does that mean, exactly? Well, since the mixin is parameterized by its superclass, it doesn't know its superclass. When the mixin is mixed into a class, it gets supplied its superclass. A mixin may be mixed in multiple times in the inheritance graph, every time with a different superclass. Note: this is exactly dual to multiple inheritance: in multiple inheritance, a class only exists once but may have many superclasses. In mixin composition, a mixin exists many times, but each instance has only one superclass.
How does this work in Ruby?
Let's again take a step back, and look at what a module in Ruby looks like operationally. A module has:
- a method table
- a constant table
- a class variable table
And what does a class look like? A class IS-A module, so it has all of the above, and in addition, it has:
What happens, when you include
a module M
into a class C
like this?
class C
include M
end
Well, first off, Module#include
is a method just like any other method. There's nothing special about it. Its default implementation looks a bit like this:
class Module
def include(mod)
mod.append_features(self)
included(mod)
end
end
So, in the end, it calls the included
hook method, but we're gonna ignore that here. It delegates the actual mixin composition to the mixin. (This means that a mixin can decide how it wants to be mixed in, by overriding the default Module#append_features
! This is only very rarely used, however, but e.g. ActiveSupport::Concern
uses it for metaprogramming.)
Now we have just pushed the question around. What does append_features
do? Well, it is again a method just like any other, it can be overridden, it can be monkey-patched, it can be removed (not a good idea!). However, its default implementation can not be expressed in Ruby.
What it does, is:
- create a new class, let's call it
M′
- set
M′
's method table pointer to M
's method table (and ditto for the constant table and class variable table)
- set
M′
's superclass pointer to C
's superclass
- set
C
's superclass pointer to M′
Effectively making M′
the new superclass of C
, and making the old superclass of C
the superclass of M′
, or put another way, insert M′
directly above C
in the inheritance tree.
Why is it done this way? Because it keeps method lookup extremely simple: the method lookup algorithm doesn't need to know about mixins at all, it is still the exact same algorithm as it would look like in a language without mixins:
- get the receiver's class pointer
- if the class has the method, execute it, otherwise, get the superclass pointer and repeat step 2
- if the class pointer is empty, call
method_missing
passing the name of the method and the arguments along (unless the name of the method is method_missing
, then raise
a NoMethodError
exception)
Note that the meat of the algorithm is simply a very tight while
loop in step 2. Tight simple loops are good, after all, method lookup is the most often executed operation in an object-oriented language implementation.
Now, you might say, "wait, I have asked C
for its superclass
and it does not return M′
, it returns Object
!" Yes, that's true. However, Class#superclass
does not simply return the superclass pointer, unlike the method lookup algorithm, it does know about mixins. Or, rather, it knows about what the developers of YARV call virtual classes, and it knows to skip them, when returning the superclass. Virtual classes are a name that is used internally in YARV. It refers to include classes (i.e. the classes I described above, which get created during include
) and singleton classes. Reflective methods know how to treat them specially, e.g. Object#class
and Class#superclass
ignore them, Module#ancestors
knows to return the module M
instead of the include class M′
.
I will pause here, to let you run the append_features
algorithm and the method lookup algorithm for your code on a piece of paper, so that you can figure out for yourself why you are seeing the results you are seeing.
Okay, so, how does that look like in your example?
We have our class ABC
which has Object
as its superclass. Out current inheritance tree looks like this:
ABC → Object
Now, we include
A
. Which means that an include class A′
gets inserted right above ABC
:
ABC → A′ → Object
We repeat the step with B
:
ABC → B′ → A′ → Object
Let's now "run" the method lookup for name
:
- get the class pointer of the object. It points to
ABC
.
- Does
ABC
have a method called name
? No!
- Get
ABC
's superclass pointer. It points to B′
- Does
B′
have a method called name
? Yes, because it shares its method table with B
which has a method called name
.
- We're done, we found it. Execute it!
I think you can see how that would work the other way round in your second code example.
Note: This answer deliberately ignores the existence of Module#prepend
and Refinements, which complicate matters somewhat.