ActiveSupport's Concern, in pictures

I spent some time digging into Rails’ ActiveSupport::Concern module. How it behaves surprised me a little bit, so I figured I’d write up what I learned.

ℹ️ If you only have a shaky understanding of include and extend in Ruby, you might want to start with this post first, which takes a deep dive into Ruby’s built-in tools for inheritance.

Consider this snippet, using plain Ruby inheritance features:

module IParent
end

module IChild
  include IParent
end

class Parent
  include IChild
end

class Child < Parent
end

We can ask Ruby to print out the ancestor hierarchy for the classes in this snippet:See the appendix for the code I used to generate these printouts

ancestors of IChild          = [IParent]
ancestors of #<Class:IChild> = [...]
────────────────────────────────────────────────────────────────────────
ancestors of Parent          = [IChild, IParent, ...]
ancestors of #<Class:Parent> = [...]
────────────────────────────────────────────────────────────────────────
ancestors of Child          = [Parent, IChild, IParent, ...]
ancestors of #<Class:Child> = [#<Class:Parent>, ...]

… but I don’t like printouts—I’d rather have pictures. So here’s a picture of the same thing:

Some conventions in this picture:

Since the snippet was plain Ruby, hopefully everything we see is familiar:

Don’t ask me why there’s all these rules, this is just how Ruby behaves.

From time to time, we might wish Ruby did something differently, allowing modules to define singleton class methods that can get mixed in alongside their instance methods. Luckily we don’t even have to wish: Ruby is nearly infinitely flexible, and so people created ActiveSupport::Concern to allow this (with only a few restrictions).

Here’s how to use ActiveSupport::Concern:

module IParent
end

module IChild
  extend ActiveSupport::Concern

  include IParent

  module ClassMethods
    # Despite being declared as an instance method,
    # this will act like a singleton class method.
    def foo; end
  end
end

class Parent
  include IChild
end

Parent.foo # ✨ magic ✨

class Child < Parent
end

In this example, let’s say that IChild wanted to declare some singleton class methods on whatever class it was eventually include’d into. All it has to do is extend ActiveSupport::Concern and then define module ClassMethodsThe ClassMethods name is special to ActiveSupport::Concern, not Ruby. It also allows defining a class_methods do ... end block, but that does nothing more than define this module and then run the block inside it.

inside itself. The library will make sure that this module gets extend’ed into whichever class eventually writes include IChild.

And to drive the point home, here’s a picture:

Beautiful, just like we’d expect.

One problem though: what if it’s not IChild that wants to define the singleton class methods, but rather IParent?

module IParent
  extend ActiveSupport::Concern

  module ClassMethods
    def bar; end
  end
end

module IChild
  include IParent
end

class Parent
  include IChild
end

Parent.bar # dang 😰

class Child < Parent
end

Suddenly this doesn’t work: the ClassMethods module gets extended at the point where the include IParent happens. That means that we’d be able to call IChild.bar, but that’s beside the point: we wanted those methods to be available as if they were singleton class methods on Parent. We can see where things went wrong in the picture:

And again, ActiveSupport::Concern has already thought of this case: it defers extending the ClassMethods all the way until the first non-Concern. So we can simply declare IChild as a Concern too (even if it doesn’t have any ClassMethods). That will ensure that IParent::ClassMethods end up on #<Class:Parent>.

It’s a single-line change:

module IParent
  extend ActiveSupport::Concern

  module ClassMethods
    def bar; end
  end
end

module IChild
  extend ActiveSupport::Concern

  include IParent
end

class Parent
  include IChild
end

Parent.bar # ✨ magic restored ✨

class Child < Parent
end

And when we look at the picture:

Wait, what is going on?

First of all, ClassMethods was only extended into Parent, not also into IChild. I suppose that makes sense; maybe there’s no real use for those ClassMethods to also end up on the (childless) singleton class of IChild. In all likelihood, those methods were going to be things like “create an instance of the current class” which is something that doesn’t make sense for module singleton class methods to be doing. I will admit that my expectation was that ClassMethods would get extend’ed onto every module singleton class that include’d the attached class, to mimic how it works when inheriting classes.

But what really surprised me: despite literally having the include IParent line in its class body, IParent is not an ancestor of IChild. 🤯

Instead, it’s only when IChild is eventually include’d into Parent that that include IParent line has an effect. We see that there are two lines coming out of Parent: one directly to IChild (makes sense), and then one that skips directly from Parent to IParent.

This is kind of wild to me,Even more wild is that it’s done all in plain Ruby, using append_features.

because it literally changes the meaning of include inside an ActiveSupport::Concern module.

Full disclosure: I don’t know why this is. If you’ll allow me to guess, I’d say because ActiveSupport::Concern is not only designed to let you mix singleton class methods into the right place, but also to register callbacks to run arbitrary code once the concern is mixed in there. These callbacks might do things like “call a DSL method in the context of a database model,” or something. The methods that callback calls won’t exist until the very end of the include chain, when the Concern gets mixed into the model.


With any luck maybe that clears things up a bit. If I’ve gotten something wrong, or this leaves you with even more questions, feel free to reach out.




Appendix: relationship with type systems

Sorbet has a similar but different feature, called mixes_in_class_methods. I don’t know the history of it, nor why it appears to work so differently from ActiveSupport::Concern despite being clearly inspired by the ClassMethods pattern. I’ll have to ask around for the history on that.

Separately, building Sorbet’s relatively new has_attached_class! feature, I kept finding it really clunky to use in combination with mixes_in_class_methods. The behavior ActiveSupport::Concern has around ClassMethods would make some annoyances with has_attached_class! go away. But at this rate, making a change to how mixes_in_class_methods is sure to break some other code. But maybe there’s a way to build better support for Concern-ish things more generally, so that new code might be able to move away from mixes_in_class_methods. I have not given this much thought.

And as one final point: by changing how include works, it changes the meaning of this expression: if IChild < IParent. This is a very not fun thing to think about the consequences of if you’re trying to build a type system for Ruby. I’m struggling to think of how to use this fact to come up with some sort of contradiction between what would be predicted by the type system and what would actually happen at runtime. If you can come up with one I’d be very curious to see it.

Appendix: support code

I wrote some random helper functions to print the “ancestors of …” information that I used to figure out how to draw the pictures in this post. Here’s the code:

Click to show code
require 'active_support/concern'

def interesting_ancestors(mod)
  non_self_ancestors = mod.ancestors[1..]
  interesting = non_self_ancestors.filter do |ancestor|
    ![
      Class, Module, Object, Kernel, BasicObject,
      ActiveSupport::Concern,
      Object.singleton_class, BasicObject.singleton_class
    ].include?(ancestor)
  end.map(&)
  if interesting.size != non_self_ancestors.size
    interesting << "..."
  end
  pretty_mod =
    if mod.singleton_class?
      "#{mod}"
    else
      "#{mod}         "
    end
  puts("ancestors of #{pretty_mod} = [#{interesting.join(", ")}]")
end

def all_interesting_ancestors(*mods)
  first = true
  mods.each do |mod|
    unless first
      puts('─' * 72)
    end
    interesting_ancestors(mod)
    interesting_ancestors(mod.singleton_class)

    first = false
  end
end

It’s not particularly pretty, but it works for the four snippets in this post.

Appendix: all ancestor information

I only showed the first printout, and then skipped straight to pictures. Here’s the printouts for all four examples:

Click to show all printouts
❯ for i in {1..4}; do echo; ruby concern$i.rb ; echo ; done

ancestors of IChild          = [IParent]
ancestors of #<Class:IChild> = [...]
────────────────────────────────────────────────────────────────────────
ancestors of Parent          = [IChild, IParent, ...]
ancestors of #<Class:Parent> = [...]
────────────────────────────────────────────────────────────────────────
ancestors of Child          = [Parent, IChild, IParent, ...]
ancestors of #<Class:Child> = [#<Class:Parent>, ...]


ancestors of IChild          = [IParent]
ancestors of #<Class:IChild> = [...]
────────────────────────────────────────────────────────────────────────
ancestors of Parent          = [IChild, IParent, ...]
ancestors of #<Class:Parent> = [IChild::ClassMethods, ...]
────────────────────────────────────────────────────────────────────────
ancestors of Child          = [Parent, IChild, IParent, ...]
ancestors of #<Class:Child> = [#<Class:Parent>, IChild::ClassMethods, ...]


ancestors of IChild          = [IParent]
ancestors of #<Class:IChild> = [IParent::ClassMethods, ...]
────────────────────────────────────────────────────────────────────────
ancestors of Parent          = [IChild, IParent, ...]
ancestors of #<Class:Parent> = [...]
────────────────────────────────────────────────────────────────────────
ancestors of Child          = [Parent, IChild, IParent, ...]
ancestors of #<Class:Child> = [#<Class:Parent>, ...]


ancestors of IChild          = []
ancestors of #<Class:IChild> = [...]
────────────────────────────────────────────────────────────────────────
ancestors of Parent          = [IChild, IParent, ...]
ancestors of #<Class:Parent> = [IParent::ClassMethods, ...]
────────────────────────────────────────────────────────────────────────
ancestors of Child          = [Parent, IChild, IParent, ...]
ancestors of #<Class:Child> = [#<Class:Parent>, IParent::ClassMethods, ...]