ActiveSupport::Concern Digression

It happened again. While reading through ActiveModel I realized that I had skipped over something cool in ActiveSupport, so I’m going to circle back and cover it.

It’s about the journey and not the destination anyway, right? And so gracious reader, we return to…

Ye Olden Days

Back in the days before we were all travelling West with Rails 4, in fact, way before them, you would find many ways discussed as best practices to extend the functionality of a Class. There is a great summary of approaches on Yehuda’s blog which I’ll just point at and leave their review as an exercise for the reader.

Clearly, with this much confusion, having a blessed way to do this would make everyone’s life a wee bit better, and so Rails extended Ruby once again with ActiveSupport::Concern.

ActiveSupport::Concern

Here is the before picture (from Rails’ docs):

module M
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end

module ClassMethods
...
end
end

And what the above looks like with ActiveSupport::Concern:

require 'active_support/concern'

module M
extend ActiveSupport::Concern

included do
scope :disabled, -> { where(disabled: true) }
end

module ClassMethods
...
end
end

In addition to a clean interface that standardized the approach, it also enabled automatic dependency resolution which we will see as we…

Read the Source!

Now that you now what features this delivers, let us take a gander at how it’s implemented.

  module Concern
class MultipleIncludedBlocks < StandardError #:nodoc:
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end

def self.extended(base) #:nodoc:
base.instance_variable_set("@_dependencies", [])
end

def append_features(base)
if base.instance_variable_defined?("@_dependencies")
base.instance_variable_get("@_dependencies") << self
return false
else
return false if base < self
@_dependencies.each { |dep| base.send(:include, dep) }
super
base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
end
end

def included(base = nil, &block)
if base.nil?
raise MultipleIncludedBlocks if instance_variable_defined?("@_included_block")

@_included_block = block
else
super
end
end
end
end

Rather than trim out boring bits, I’ve included the whole source of the module. Everything to safely add both Class and Instance methods (and all relevant dependencies) are in the above. Some review…

Module#included

Callback invoked whenever the receiver is included in another module or class. This should be used in preference to Module.append_features if your code wants to perform some action when a module is included in another.

Looking then at the code above, we see that there’s an exception thrown if it encounters something like:

module Foo
extend ActiveSupport::Concern

included do
...
end
included do
...
end
end

A condition it easily detects via an instance variable it sets on the first one.

Module#extended

Called when this module extends another. In the case of Concern it just uses this as an initialization hook for the creation of the @_dependencies array.

Ok, on to the real logic!

Module#append_features

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.

So let’s say we had a scenario where we had some top level class Zoo, which includes module Foo, which folds in Bar, and where Bar secretly depends on Baz. Like so:

module Baz
extend ActiveSupport::Concern
def baz
puts "baz!"
end
end

module Bar
extend ActiveSupport::Concern
include Baz
def bar
puts "bar!"
end
end

module Foo
extend ActiveSupport::Concern
include Bar
end

class Zoo
include Foo
end

If we inspect this hypothetical tree, we’ll see that @_dependencies points to the next level of the tree:

Foo.instance_variable_get("@_dependencies") == [Bar]
Bar.instance_variable_get("@_dependencies") == [Baz]

And that every method was similarly added to Zoo

Zoo.new.bar == 'bar!'
Zoo.new.baz == 'baz!'

So the big question is, how does this code work? Let’s take my example above and hack in some debugging statements:

module ActiveSupport
module Concern
def self.extended(base) #:nodoc:
base.instance_variable_set("@_dependencies", [])
end

def included(base = nil, &block)
unless base.nil?
super
end
end

def append_features(base)
if base.instance_variable_defined?("@_dependencies")
puts "Creating @_dependencies on - #{base.to_s}"
base.instance_variable_get("@_dependencies") << self
return false
else
if base < self
puts "Nothing additional for - #{base.to_s}"
return false
else
puts "Including dependencies #{@_dependencies.inspect} to - #{base.to_s}"
end
@_dependencies.each { |dep| base.send(:include, dep) }
super
puts "Checking for ClassMethods and blocks in - #{base.to_s}"
base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
end
end
end
end

Running this, we see the following:

Creating @_dependencies on - Bar
Creating @_dependencies on - Foo
Including dependencies [Bar] to - Zoo
Including dependencies [Baz] to - Zoo
Including dependencies [] to - Zoo
Checking for ClassMethods and blocks in - Zoo
Checking for ClassMethods and blocks in - Zoo
Checking for ClassMethods and blocks in - Zoo

Which makes it even more obvious than the code (which wasn’t very obvious if I’m being quite honest). The bookkeeping on dependent modules is only triggered when we decorate the eventually inclusive class (Zoo above). Since it is the destination and doesn’t have a @_dependencies set, it finally goes down the other branch to trigger a bunch of include statements.

Easy to see in retrospect, but I needed to “lab” this one.

Have fun reading the source!

Published under programming