append_features(mod)

append_features(mod)

The documentation says:

When this module is included in another, Ruby calls append_features in this module, passing it the receiving module in mod. Ruby’s default implementation is to add the constants, methods, and module variables of this module to mod if this module has not already been added to mod or one of its ancestors. See also Module#include.

What if this module is included in a class, will append_features of this module still be called, passing in the class as mod? This is in particular reference to ActiveRecord::Concern, which has its own implementation of append_features overriding ruby's.

1.9.3p0 :003 > class A 1.9.3p0 :004?> end 1.9.3p0 :009 > A.is_a?(Module) => true 1.9.3p0 :010 > A.is_a?(Class) => true 1.9.3p0 :011 > Class.is_a?(Module) => true

class is module, so answer is append_features is run

So that explains a lot.

When module B is included in class C, we extend ActiveSupport::Concern, which invokes the extended hook in the module Concern, passing module B as local variable base. We want to indicate that module B is a concern, so we set an instance variable @_dependencies on it set to an empty array. Next, since B is included in class C, the included hook in module B is called. The included hook of ruby is overwritten by the included hook of ActiveSupport::Concern. It checks if base was passed as the first argument to the included hook, if not, the block passed to the included hook in module B (which was included in class C) is set to the @_included_block instance variable of Concern. Finally, since a class is an instance of Module, a class is a module. Hence, since we included module B in class C (which itself is a Module), the append_features hook is executed. What append_features does is that when this module is included in another, Ruby calls append_features in this module, passing it the receiving module in mod. ActiveSupport::Concern captures the append_features call and implements its own definition. The class C is passed as base to append_features when module B is included in class C, and we check if class C has the @_dependencies instance variable defined. Since we did not extend ActiveSpport::Concern in class C, it does not have that instance variable set, so the else clause is, in effect, triggered. What we then do is iterate through each of the modules stored in @_dependencies and include them in class C (base). We can have multiple modules in the @_dependencies instance variable, and we have them all included in base here, so that if one depends on another, they are all available now in class C. It then extends ClassMethods and includes InstanceMethods if they are defined in module B. It finally invokes class_eval method on base (class C), passing in the instance variable @_included_block if it is defined. @_included_block stores the block that we passed into included call in module B, if we did not pass an argument (other than the block) to the included call. So that block gets evaluated in the class context of C, and hence we can call class methods within that block. Next notice that module A is included in module B. Ruby now evaluates module A. The extended method is called, and it points to ActiveSupport::Concern again. So the extended hook of Concern is invoked, we pass module A as local variable base, and we set the @_dependencies instance variable to an empty array. However, there is no included hook in module A, so that is skipped. Ruby then invokes append_features, since module A was included in module B. appended_features gets passed the receiving module (module B) in module A context. We check if module B has the @_dependencies instance method defined. It does actually (since remember in module B we extended ActiveSupport::Concern which we did not do in class A). So since module B has that instance variable set, we simply include self (module A) into the array of modules held in @_dependencies array. Hence, we can have a ton of modules stored in @_dependencies by following the same process. Ultimately, all of the modules will be included in the first module or class that does not extend ActiveSupport::Concern. The reason why this is done is so that module dependencies will be included when the module is included in the parent class (or module). Apparently, ruby has no built in utility to accomplish dependency resolution when one module depends on others and that one module is included in a class.