Down the Ruby Mine, Part I: The code insertion trinity

Posted by on August 16, 2019

Hey there, my name’s Sam and I am one of four software engineering interns working at FreeAgent over the summer. This is my first time writing in Ruby and I’ve had a great time exploring the language. As developers I believe it’s important to develop a fundamental understanding of the core of a language, even when its supplemented by a feature-rich framework like Rails.

Over the next few weeks I’ll be releasing a series of blog posts exploring an eclectic bunch of language features that I’ve encountered during my time at FreeAgent. The function of these features were not clear to me upon first inspection and I hope that by sharing their inner workings with you, we will all end up closer to enlightenment. Time to take a good ol’ fashioned spelunk into the magical, awe-inspiring depths of Ruby.

One thing I first noticed navigating the FreeAgent codebase was the ubiquitous use of the include statement. When it first caught my eye I froze and a cold feeling permeated my body; the fear of the unknown. However, once I had recovered, I realised it looked similar to constructs in languages I was familiar with. It was reminiscent of the import keyword in Python or Java used to get code from one file into another file – a quick google search confirmed this. Venturing on with renewed courage I soon encountered extend and reeled out of my chair in fright. To prevent further office disruptions, I decided it was time to flesh out these keywords once and for all.

Include and extend are closely related to a third keyword, prepend, which I’ll explain for completeness. All three keywords are used to add functionality from one module into a class or another module. The differences between them lie in the way they interact with the class/module being added to.

Before we go into how they work it’s important to understand Ruby’s class hierarchy. Ruby’s object oriented design results in each class having a list of ancestors that form a hierarchy of inheritance. The top-most ancestor of all objects is the BasicObject class — since Ruby 1.9 — while the bottom-most is (almost always) the class itself. Let’s take a look at the ancestors of the friendly Array class:

The Array class is a descendant of many other ruby classes from which it inherits functionality

If you don’t specify a parent class when creating a class in Ruby, the class will implicitly inherit from Object. Object’s parent is Kernel and Kernel’s parent is BasicObject so don’t be alarmed when you find Object, Kernel and BasicObject lurking in the ancestor hierarchy of most Ruby classes.

With the ancestor hierarchy in mind we can begin to dive into the include keyword. Include inserts the included module as a parent into a class or module. The included module’s methods are then accessible as instance methods. When inserted into a class this means that objects of that class can call the included module’s methods. The code below illustrates how this works:

module IncludedModule
  def was_included
    puts "Hello, I am from the IncludedModule"
  end
end
class MainClass
  include IncludedModule
end
MainClass.ancestors
# [MainClass, IncludedModule, Object, Kernel, BasicObject]

mc = MainClass.new
mc.was_included
# Hello, I am from the IncludedModule

We can see that the IncludedModule has been inserted as a parent to MainClass. IncludedModule’s methods are accessible to MainClass objects, meaning they are now instance methods of MainClass.

Note that as modules cannot be instantiated, including a module in another module does not have an obvious effect (besides altering the module’s ancestors). However, the included module’s methods are still added as instance methods. So when a class includes this module, it will allow objects of the class to access both the module’s methods and the methods of the module, the module included. If at this point you feel sick of the word “module”, let me apologise and lay it out visually:

If B includes A and C includes B then an object d, of class C, can use methods from A and B

Extend works like include but instead of adding included methods as instance methods, it adds them as class or module methods. This means that they can be accessed from the class or module but not from an instance:

module ExtendedModule
  def was_extended
    puts "Hello, I am from the ExtendedModule"
  end
end
class MainClass
  extend ExtendedModule
end
MainClass.ancestors
# [MainClass, Object, Kernel, BasicObject]

MainClass::was_extended
# Hello, I am from the ExtendedModule

mc = MainClass.new
mc.was_extended
# NoMethodError

Extend is unique in that it does not change the inheritance hierarchy

module MainModule
  extend ExtendedModule
end
MainModule::was_extended
# Hello, I am from the ExtendedModule

As a bonus it allows modules to share each other’s methods

Last but not least is prepend. Prepend works in a similar way to include but instead of the inserted module being added as a parent in the inheritance hierarchy, it is added as a child. This means that if there are overlapping methods between the included module and the main class/module the included methods override the others:

module PrependedModule
  def was_prepended
    puts "Hello, I am from the PrependedModule"
  end
end
class MainClass
  prepend PrependedModule

  def was_prepended
    puts "Hello, I am from the MainClass
  end
end
MainClass.ancestors
# [PrependedModule, MainClass, Object, Kernel, BasicObject]

mc = MainClass.new
mc.was_prepended
# Hello, I am from the Prepended Module

As the prepended module sits at the bottom of the hierarchy, its method gets called instead of the method defined in the class

Hopefully this has taught you something new about Ruby’s code insertion keywords. Moving code around programmatically is often overlooked, but knowing exactly the right keyword for your use-case could prevent a few headaches down the line. Stay tuned for another post next week, I’ve lined up something that will have you on the edge of your seat: logical operators.

Leave a reply

Your email address will not be published.