This post continues to look at Ruby Modules and their properties.
It’s possible to mix a Module into a second Module which can then be mixed into a class. The result is that an entire Module chain is moved into the a classes inheritance chain with one call to include.
Although this sort of mixin chaining is generally a bad practice which can lead to overly complex inheritance chains, it’s important in understanding how include works. rb_include_module is a complex function. As a first pass at it we’ll only look at the basic include loop…
The variable c here is used to store the current point of insertion. The first time through the loop c points to the klass passed into the function.
In the Web example above c is set to Web and module is TwoStrand before entering the while loop. An include class is created for TwoStrand with a super pointer to Web’s super, Object. Web’s super is set to TwoStrand. Since any additional Modules to be inserted should be inserted above TwoStrand, the point of insertion, c, is set to the newly created TwoStrand include class. On the next line module is set to TwoStrand’s super, OneStrand. The next time through the loop a new include class is created for TwoStrand and it’s super is set to the super of the OneStrand include class, Object. The OneStrand include class’s super is updated to point to the new TwoStrand include class and once again the point of insertion is moved. Eventually super returns a null pointer and the loop exits.
An interesting property of the include class architecture is that an include class does not keep track of changes to the inheritance chain of the Module it “wraps”.
Although Strand’s super now points to Tangled, the Include class created for Strand when it was mixed into Web is unaffected by the change. This is known as the Dynamic Module Include Problem. This makes sense if one bears in mind that the Strand Module in Web’s inheritance chain is actually an Include class. Inclusion of Tangled within the Strand Module means that the the super pointer of Strand now points to an Include class for Tangled. Any classes or Modules that includes Strand from here out will pull a new Include class that points to Strand’s Tangled Include class into their inheritance chain when rb_include_module. Existing Include classes for Strand however remain blissfully unaware that their “real” Module now has an altered inheritance chain.
Part of the code that we did not look at in our first pass through rb_include_module is responsible for ensuring a Module is only included once in a classes inheritance chain.
Note that OneStrand was not inserted twice and did not change it’s position in the inheritance hierarchy. Let’s add some code back into rb_include_module to see how this is implemented.
A for loop has been introduced inside of the while loop. For each Module in module’s super chain the inheritance chain of klass is analyzed looking for a class whose method table is the same as the current Module’s method table. This method table comparison is a slightly non-intuitive way of checking if the current inheritance chain includes either the actual Module being considered, or an include class mapping to that Module. In either case the method tables would be the same, and would indicate that this Module is already included. If so, the include class creation is skipped and the while loop continues to the next Module being considered for inclusion. Note that this means that while a Module may only be included once, that does not mean that a re-include will not have side effects. Specifically, new additions to the Modules inheritance chain will be picked up by a re-include.
The TwoStrand Module is skipped over because its method table pointer is the same as the existing include class in Web’s super chain. When super is called on TwoStrand this time around, Tangled is returned. Since Tangled isn’t currently part of Web’s inheritance chain, a new include class is created for it and inserted immediately above TwoStrand.
It is possible for a Module to be included into an inheritance chain twice if it included into a super class after it has been included into a subclass.
Blacksmith’s super points to a Doppelganger Include class which points to Craftsman at the time Doppelganger is included into Craftsman. Craftsman has no knowledge of what classes may point to it and gamely allows its super to be set to a new Include class for Doppelganger. It’s hard to imagine a situation where you would want this behavior, but understanding how it can happen may help debug a bizarre situation.
The next (and last post) on Modules will cover extend and class methods. As always, thanks for reading.