I was reading through Metaprogramming Ruby 2 last night and had a thought that stuck with me. Specifically, it had to do with the examples of module inclusion and class ancestor chains and the possibility of using combinations of prepended and included modules to simulate multiple inheritance hierarchies. Precisely one nap later, my subconscious worked out the pitfalls and best practices for that type of design pattern, leaving me to wonder about matters of implementation. Naturally, my thoughts turned to a staple of the inheritance hierarchy – super. I fired up pry and gave it a shot.
module TestModule1
def test_super
puts "In the prepended module!"
super
end
end
module TestModule2
def test_super
puts "In the included module!"
super
end
end
class TestParentClass
def test_super
puts "In the parent class!"
end
end
class TestChildClass < TestParentClass
prepend TestModule1
include TestModule2
def test_super
puts "In the child class!"
super
end
end
TestChildClass.ancestors
=> [TestModule1, TestChildClass, TestModule2, TestParentClass, Object, ...]
TestChildClass.new.test_super
In the prepended module!
In the child class!
In the included module!
In the parent class!
=> nil
It works! Yay! Should this be surprising? Depending on your sources of information, yes or no. Stack overflow answers, internet comments and even books typically describe the function of super as working on the superclass. To quote The Ruby Programming Language, “super works like a special method invocation: it invokes a method with the same name as the current one, in the superclass of the current class. (Note that the superclass need not define that method itself—it can inherit it from one of its ancestors.)”
This hints at the true functionality of the super keyword, sort-of, yet the following text references super as working on the superclass rather than the next ancestor up the hierarchy. There’s no mention at all of a module implementing the identically named method between the subclass and superclass.
Unsurprisingly, people simplify the information and think of super as forcing the method lookup to jump directly to the superclass. It makes sense. That concept is consistent with concepts of inheritance hierarchy and even the ancestors chain of a class… Until we get to module mix-ins. Now everything is different. There are new ancestors in the chain that do not follow strict inheritance hierarchies.
This leads to shouldn’t-be-confusing bugs when you call super in a method and that call gets unintentionally intercepted by a module mixin. You look in the subclass, see no problems. You look in the superclass, you see no problems. If you happen to intercept the method in a way that doesn’t cause an exception to be thrown then you might be in for a debugging ride, crawling through the call stack one item at a time, looking for anything that’s out of place. Once you’re out of that theatre of pain, you might be put off from using super, module mixins or both. Save yourself that pain, remember the interaction of modules and the super keyword.
The most interesting (and dangerous) interaction of modules and super comes from the fact that Ruby either will or will not ignore multiple module inclusions, depending on how your code is organized and loaded. This makes for an opportunity for bugs as well as an opportunity to sadistically inflict pain on others with obscure code that abuses bugs in the programming language to achieve desired behavior. Make sure you remain aware of this behavior when using the super keyword in conjunction with modules.
Run the following code to see for yourself:
module Mod;end
class Test1;end
class Test2 < Test1
include Mod
end
p Test1.ancestors, Test2.ancestors
class Test1
include Mod
end
p Test1.ancestors, Test2.ancestors
Future Ruby versions might behave differently, according to the discussion in the Ruby issue tracker and the older related issue. Should be a lot of fun if we get a more dynamic ancestors tree!