Rails concern method override another concern method doesn't work like normal modules

user1011792 picture user1011792 · Feb 25, 2016 · Viewed 7.9k times · Source

Let's say I have the following structure in ruby (no rails)

module Parent
    def f
        puts "in parent"
    end
end

module Child
    def f
        super
        puts "in child"
    end
end

class A
    include Parent
    include Child
end

A.new.f # prints =>
#in parent
#in child

Now when using rails concerns

module Parent
  extend ActiveSupport::Concern

  included do

    def f
      puts "In Parent"
    end


  end

end

module Child
  extend ActiveSupport::Concern

  included do

    def f
      super
      puts "In Child"
    end


  end

end

class A < ActiveRecord::Base
  include Parent
  include Child
end

A.new.f #exception
NoMethodError: super: no superclass method `f' for #<A:0x00000002244490>

So what am I missing here? I need to use super in concerns like in normal modules. I searched but I could not find help on this topic

Answer

BroiSatse picture BroiSatse · Feb 25, 2016

The reason for this is that included method block is actually evaluated in the context of the class. That mean, that method defined in it is defined on a class when module is included, and as such takes precedence over included modules.

module Child1
  extend ActiveSupport::Concern  
  included do   
    def foo
    end    
  end
end

module Child2
  def bar
  end
end

class A
  include Child1
  include Child2
end

A.new.method(:foo).owner   #=> A
A.new.method(:bar).owner   #=> Child2

Method lookup

In ruby, every time you want to call a method, ruby has to find it first (not knowing whether it is method or a variable). It is done with so called method lookup. When no receiver is specified (pure call like puts) it firstly searches the current scope for any variables. When not found it searches for that method on current self. When receiver is specified (foo.bar) it naturally search for the method on given receiver.

Now the lookup - in ruby all the methods always belongs to some module/class. The first in the order is receiver's eigenclass, if it exists. If not, regular receiver's class is first.

If the method is not found on the class, it then searches all the included modules in given class in the reversed order. If nothing is found there, superclass of given class is next. The whole process goes recursively until something is found. When lookup reaches BasicObject and fails to find the method it quit and triggers search for method_missing, with default implementation defined on BasicObject.

Important thing to notice is that methods which belongs to the class always take precedence over module methods:

module M
  def foo
    :m_foo
  end
end

class MyClass
  def foo
    :class_foo
  end

  include M
end

MyClass.new.foo    #=> :class_foo

About super

Search for a super method is very similar - it is simply trying to find a method with the same name which is further in the method lookup:

module M1
  def foo
    "M1-" + super
  end
end

module M2
  def foo
    'M2-' + super
  end
end

module M3
  def foo
    'M3-' + super
  end
end

class Object
  def foo
    'Object'
  end
end

class A
  include M2
  include M3
end

class B < A
  def foo
    'B-' + super
  end

  include M1
end

B.new.foo    #=> 'B-M1-M3-M2-Object'

ActiveSupport::Concern#included

included is a very simple method that takes a block and creates a self.included method on the current module. The block is executed using instance_eval, which means that any code in there is actually executed in the context of the class given module is being included in. Hence, when you define a method in it, this method will be owned by the class including the module, not by the module itself.

Every module can hold only one method with given name, once you tries to define second one with the same name, the previous definition is completely erased and there is no way it can be find using ruby method lookup. Since in your example you included two modules with same method definition in included block, the second definition completely overrides the first one and there is no other definition higher in method lookup, so super is bound to fail.