I thought that including a module as a mixin in a class "added the functions" to the class.
I do not understand why this does not work as expected:
module A
def blah
super if defined?(super)
puts "hello, world!"
end
end
class X
include A
end
class Y < X
include A
end
y = Y.new
y.blah
I was expecting "y" to call its super blah() (since its included in class X?) but instead i got:
test.rb:3:in blah: super: no superclass method `blah'
You're running into nuances of Ruby's object hierarchy and how method lookups interact with included modules.
When you invoke a method on a object, Ruby walks over the ancestors list for the object's class, looking for an ancestor class or module that responds to that method. When you invoke super in that method, you're effectively continuing your walking up the tree of ancestors, looking for the next object that responds to the same method name.
The ancestor tree for your X and Y classes look like this:
p X.ancestors #=> [ X, A, Object, Kernel, BaseObject ]
p Y.ancestors #=> [ Y, X, A, Object, Kernel, BaseObject ]
The problem is that includeing the module a second time, in a child class, does not inject a second copy of the module in the ancestors chain.
Effectively what is happening is when you invoke Y.new.blah, Ruby begins looking for a class that responds to blah. It walks past Y, and X, and lands on A which introduces the blah method. When A#blah invokes super, the "pointer" into your ancestor list is already pointing at A, and Ruby resumes looking from that point for another object responding to blah, starting with Object, Kernel, and then BaseObject. None of these classes have a blah method, so your super invocation fails.
A similar thing happens if a module A includes a module B, and then a class includes both module A and B. The B module is not included twice:
module A; end
module B; include A; end
class C
include A
include B
end
p C.ancestors # [ C, B, A, Object, Kernel, BaseObject ]
Note that it's C, B, A, not C, A, B, A.
The intent would seem to be to allow you to safely invoke super inside any of A's methods without worrying about how consuming class hierarchies may inadvertently include A twice.
There are a few experiments that demonstrate different aspects of this behavior. The first is adding a blah method to Object, which allows the super call to pass:
class Object; def blah; puts "Object::blah"; end; end
module A
def blah
puts "A::blah"
super
end
end
class X
include A
end
class Y < X
include A
end
Y.new.blah
# Output
# A::blah
# Object::blah
The second experiment is to use two modules, BaseA and A, which does cause the modules to be inserted twice, correctly, in the ancestors chain:
module BaseA
def blah
puts "BaseA::blah"
end
end
module A
def blah
puts "A::blah"
super
end
end
class X
include BaseA
end
class Y < X
include A
end
p Y.ancestors # [ Y, A, X, BaseA, Object, ...]
Y.new.blah
# Output
# A::blah
# BaseA::blah
A third experiement uses prepend, instead of include, which places the module in front of the object in the ancestors hierarchy and interestingly does insert a duplicate copy of the module. This allows us to reach the point where effectively Y::blah invokes X::blah, which fails because Object::blah does not exist:
require 'pry'
module A
def blah
puts "A::blah"
begin
super
rescue
puts "no super"
end
end
end
class X
prepend A
end
class Y < X
prepend A
end
p Y.ancestors # [ A, Y, A, X, Object, ... ]
Y.new.blah
# Output
# A::blah (from the A before Y)
# A::blah (from the A before X)
# no super (from the rescue clause in A::blah)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With