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 include
ing 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