Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling "super" keyword with modules and inheritance

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'

like image 757
d0bz Avatar asked Dec 23 '22 22:12

d0bz


1 Answers

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)
like image 91
meagar Avatar answered Dec 31 '22 01:12

meagar