Calling "super" keyword with modules and inheritance

Question

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'


Show source
| ruby   | module   | inheritance   | super   | mixins   2017-01-03 20:01 1 Answers

Answers ( 1 )

  1. 2017-01-03 22:01

    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)
    
◀ Go back