Ruby has two class-level methods for adding modules to a class or module’s ancestor chain: the more popular Module#include
method and the less used Module#prepend
. We’re going to go over all the fun details of this method works under the hood, but let’s start there so we have a common understanding of the basic functionality.
Module#include
First, let’s talk about include
. include
inserts the specified module into the ancestor list of the receiver after the receiving class/module itself. Here’s an example:
class Goalie
end
Goalie.ancestors
#=> [Goalie, Object, Kernel, BasicObject]
module Pads
end
Goalie.include Pads
Goalie.ancestors
#=> [Goalie, Pads, Object, Kernel, BasicObject]
In this example we see that including Pads
in the Goalie
class inserts it after Goalie
in the ancestors list. This means that when you call methods on instances of Goalie
, methods defined on Goalie
will override methods on Pads
.
Module#prepend
prepend
is a more special purpose tool, but does fundamentally the same thing. However, instead of putting the module after the receiver in the ancestor chain, the new entry will be before the receiver. Let’s take a look:
class Robot
end
Robot.ancestors
#=> [Robot, Object, Kernel, BasicObject]
module AsimovSafetyOverrides
end
Robot.prepend AsimovSafetyOverrides
Robot.ancestors
#=> [AsimovSafetyOverrides, Robot, Object, Kernel, BasicObject]
The scenario is almost identical to the previous example, but in this case methods defined in AsimoveSafetyOverrides
will take precedence over methods in the Robot
class, hopefully keeping instances of Robot
obedient so long as they do not harm humans.
Under the hood, both include
and prepend
(by default) fundamentally do the same thing: they ask the argument (the module you pass in) to append or prepend its “features” onto the receiver and then call a callback on the operand with the receiver. There’s a lot of jargon in there, so let’s look at an example:
module Dog
def self.append_features(mod)
puts "append_features: #{mod.inspect}"
end
def self.included(mod)
puts "included: #{mod.inspect}"
end
end
class Roxie
include Dog
end
# Output:
append_features: Roxie
included: Roxie
Roxie.ancestors
#=> [Roxie, Object, Kernel, BasicObject]
In this example we see that when we include the Dog
module in the Roxie
class that both the append_features
and included
hooks are call on the Dog module, but with the receive, Roxie
, as an argument. Module#prepend
works the same way, but uses the prepend_features
and prepended
hooks.
The difference between this example and the previous ones is that we’ve overridden the default behaviour of append_features
. This method is responsible for actually adding Dog
to the ancestor list of Roxie
, so we can see when we inspect the ancestors, it isn’t present.
append_features
is what actually does the “including”. It’s implemented in C, but it does some basic error checking to make sure the module hasn’t already been included and clears some internal method caches and stuff.
That said, you can totally override it if you want. That’s the magic of Ruby; if the mood strikes you right, you can totally break the language in unpredictable and sometimes hilarious ways.
If you’re wondering why append_features
exists and why you might ever want to override it, here’s something you definitely shouldn’t do:
class Module
alias_method :really_append_features, :append_features
def append_features(mod)
mod.send :really_append_features, self
mod.send :included, self
end
end
module A
end
module B
include A
end
B.ancestors
#=> [B]
A.ancestors
#=> [A, B]
Ruby truly is a dynamic language. Here I’ve redefined Module#append_features
to do the opposite of what it’s supposed to do. Now when you attempt to include module A
into module B
, it instead includes module B
into module A
. If you ever do this in a real application you’ll be immediately jailed without a trial.
Luckily, in a normal application you should probably never have to override this method. The only real use I know for it is in ActiveSupport::Concern which provides a more Rails-y API for creating mixins and ensuring dependencies are handled correctly.
When you include a module into another class or module, its constants come along with it. Surely that’s also the responsibility of append_features
, right? Consider:
module Dog
BARK = "woof!"
end
class Roxie
include Dog
end
Dog::BARK
#=> "woof!"
Roxie::BARK
#=> "woof!"
This is handy, but it’s not something include
(or prepend
for that matter) do, so much as it’s a side-effect of how constant lookup works in Ruby. Ruby’s constant lookup is somewhat unintuitive and topic for another blog post, but what you need to know here is that this is the effect of Ruby continuing up the inheritance chain looking for the constant. If you were to set Roxie::BARK = "blork!"
you would not receive a warning that you were overwriting an existing constant, nor would it change the value of Dog::BARK
.
prepend
?I’ve not found many different uses for prepend
, but there’s one I use all the time. I work on Solidus and Spree apps. Both Solidus and Spree are Rails engines that provide dozens of models that comprise a full-featured eCommerce system complete with digital storefront, API, and admin interface.
Ideally all customization of the engine would be done through configuration and hooks, but the real power of these systems is that you can leverage Ruby’s dynamic features to modify the core classes however you like. For much of the history of these projects it was commonplace to simply class_eval
the core classes and overwrite whatever functionality you wanted.
This has downsides, even beyond the fact that you’re modifying classes that you don’t “own”. One of the big issues is that it can cause load order issues and breaks super
.
Fortunately, with the introduction of Module#prepend
in Ruby $INSERT_RUBY_VERSION, we were able switch to using prepend
, allow us to use super
to access the original behaviour of the classes and generally making debugging easier.
You should definitely avoid modifying classes like this whenever possible, but if you find yourself in a sticky situation where you need to, prepend
can help you out.
Object#extend
Up to this point, I’ve not mentioned the extend
method. That’s because it’s actually just a special case of include
. These two lines are functionally equivalent:
SomeClass.extend SomeModule
SomeClass.singleton_class.include SomeModule
This why extend
is defined on Object
whereas include
and prepend
are defined on Module
: include
and prepend
need the receiver to be a class or module, but extend
just needs the receiver to have a singleton class, which the vast majority of objects do.
Normally extend
is used to add functionality to a class. SomeClass.extend SomeModule
adds the methods defined on SomeModule
as “class methods” on SomeClass
. Because you can use extend with any object, this is just a special case of this behaviour of extend
: some_object.extend(SomeModule)
effectively adds the methods defined in SomeModule
to the object some_object
, regardless of whether some_object
is a class, a module, or any other kind of object.
module Barking
def bark!
"woof!"
end
end
class Dog; end
roxie = Dog.new
roxie.extend Barking
roxie.bark!
#=> "woof!"
cessna = Dog.new
cessna.bark!
# NoMethodError (undefined method `bark!' for #<Dog:0x00007f971e120888>)
As you can see, we can use extend
on an arbitrary instance to add methods to it. Functionally speaking, this adds the module to the ancestor chain of the singleton class of the receiver.
If you dig into it extend
follows the pattern we’ve seen with the other methods. It’s callback is called extended
and the method that does the heavy lifting (the equivalent of append_features
/prepend_features
) is called extend_object
. extned_object
relies on the same logic as include
does under the hood.
include
, extend
, and prepend
are all useful tools in your Ruby toolbox. Fundamentally they all provide different ways to inject new behaviour into a give class, module or object. By leveraging callbacks you can write modules that provide rich new features to the classes that use them. I hope I’ve provided you a more in-depth understanding of how these methods work that goes beyond the superficial behaviours often cited.