Blog
0 pixels scrolled
  • Home
  • Work
  • Services
  • About
  • Blog
  • Contact
Ruby's Include and Prepend:
The Stuff You Didn't Know
Jared Norman on April 10, 2019
Ruby's Include and Prepend:
The Stuff You Didn't Know
Jared Norman on April 10, 2019
Posted in Ruby
← Back to the blog

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.

“Features”

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.

Don’t Try This At Home In Production

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.

What About Constants?

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.

The Truth About 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.

In Conclusion

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.

Work ServicesAboutBlogCareersContact


Privacy policyTerms and conditions© 2019 Super Good Software Inc. All rights reserved.