There is "super" keyword in Ruby that is looking through the ancestors chain in order to find the first method implementation up the chain and execute it. So, this is how it works in Ruby, no surprises:
module Mammal
def walk
puts "I'm walking"
end
end
require '~/Documents/rubytest/super/mammal.rb'
class Cat
include Mammal
def walk
super
end
end
2.7.0 :001 > simba = Cat.new
2.7.0 :002 > simba.walk
I'm walking
=> nil
Which is the desirable behavior. Now, in Rails there is ActiveSupport::Concern that provides some extra functionality for Modules. Here's what happens if you do kind of similar way using ActiveSupport helpers:
module MammalMixin
extend ActiveSupport::Concern
included do
def show
@mammal = Mammal.find(params[:id])
end
end
end
class SomeController < ApplicationController
include MammalMixin
def show
super
end
end
If you reach that controller, this will error out: super: no superclass method `show' for #SomeController:0x000055f07c549bc0
Of course, it's possible to not use "included do" helper and revert to plain Ruby style, but could someone please suggest what exactly in ActiveSupport::Concern prevents "super" from working fine and (maybe) explain the rationale behind this?
I've been looking through the source code in active_support/concern.rb, but failing to understand.
The answer is right in the documentation of ActiveSupport::Concern#included
[bold emphasis mine]:
Evaluate given block in context of base class, so that you can write class macros here.
So, here's the content of your block:
def show @mammal = Mammal.find(params[:id]) end
And this block is evaluated in the context of the base class, as per the documentation. Now, what happens when you evaluate a method def
inition expression in the context of a class? You define a method in that class!
So, what you are doing here is you define a method named show
in the SomeController
class as if you had written:
class SomeController < ApplicationController
def show
@mammal = Mammal.find(params[:id])
end
def show
super
end
end
In other words, your second definition is overwriting the first definition, not overriding it, and so there is no super method.
The correct way to use ActiveSupport::Concern#included
is like this:
module MammalMixin
extend ActiveSupport::Concern
def show
@mammal = Mammal.find(params[:id])
end
included do
acts_as_whatever
end
end
ActiveSupport::Concern#included
, as the documentation says, is for executing code (such as "class macros" like acts_as_*
, has_many
, belongs_to
, etc.) in the context of the class.
Here's how including a module normally works:
When you write
class C
include M
end
You are calling the Module#include
method (which is not overriden by Class
and thus inherited without change).
Now, Module#include
doesn't actually do anything interesting. It basically just looks like this:
class Module
def include(mod)
mod.append_features(self)
end
end
This is a classic Double Dispatch idiom to give the module full control over how it wants to be included into the class. While you are calling
C.include(M)
which means that C
is in control, it simply delegates to
M.append_features(C)
which puts M
in control.
What Module#append_features
normally does, is the following (I will describe it in pseudo-Ruby, because the behavior cannot be explained in Ruby, since the necessary data structures are internal to the engine):
class Module
def append_features(base)
if base.is_a?(Module)
base.included_modules << self unless base.included_modules.include?(self)
else
old_superclass = base.__superclass__
klazz = Class.new(old_superclass)
klazz.__constant_table__ = __constant_table__
klazz.__class_variable_table__ = __class_variable_table__
klazz.__instance_variable_table__ = __instance_variable_table__
klazz.__method_table__ = __method_table__
klazz.__virtual__ = true
base.__superclass__ = klazz
end
included(base)
self
end
end
So, what happens is that Ruby creates a new class, called an include class whose constant table pointer, class variable table pointer, instance variable table pointer, and method table pointer point to the constant table, class variable table, instance variable table, and method table of the module. Basically, we are creating a class that shadows the module.
Then it makes this class the new superclass of the class, and makes the old superclass the superclass of the include class. Effectively, it inserts the include class between the class and the superclass into the inheritance chain.
This is done this way, because then the method lookup algorithm doesn't need to know anything about mixins and can be kept very simple: go to the class, check if the method exists, if not fetch the superclass, check if the method exists, and so on, and so forth. Since method lookup is one of the most common and most important operations in an execution engine for an OO language, it is crucial that the algorithm is simple and fast.
This include class will be skipped by the Class#superclass
method, so you don't see it, but it will be displayed by Module#ancestors
.
And that is why super
works: because the module literally becomes a superclass.
We start off with with C < Object
and we end up with C < M' < Object
.
Now, ActiveSupport::Concern
completely screws with this.
The interesting part of the ActiveSupport::Concern#included
method is this:
@_included_block = block
It simply stores the block for later use.
As I explained above, when MammalMixin
gets included into SomeController
, i.e. when SomeController.include(MammalMixin)
gets called, SomeController.include
(which is Module#include
) will in turn call MammalMixin.append_features(SomeController)
. MammalMixin.append_features
in this case is ActiveSupport::Concern#append_features
, and the most interesting part is this:
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
As you can see, it is using Module#class_eval
in order to evaluate the block it saved earlier in the context of the base class it is included into. And that's what makes your method end up as an instance method of the base class instead of the module.