An Introduction to Ruby Modules

A not so gentle Introduction

Modules are some of the most exciting concepts in Ruby, but it is also a feature that is not really understood by its users. For someone who is just starting out with the language and even to seasoned programmers, Ruby Modules can be confusing, especially when used in conjunction with classes.

Before we start on this post, let me give you a 10,000ft over-view of how this post is structured. The post takes you through

  • Different ways of using modules
  • Anonymous Modules and a Rails use case
  • In closing, a brief history of module system

How can I use modules?

Enhancing the behavior of an instance.

A good way to enhance the capabilities of an instance is to 'mix-in' a module into its class.

module Saveable
  def save
  end
end

class XMLDocument
  include Saveable
end

:0> xml_document = XMLDocument.new
:0> xml_document.respond_to? :save
=> true

Here, when Saveable module is included, the behavior of the original XMLDocument changes. It can be saved to the disk now.

Another way to look at this is, now instance-methods of XMLDocument can access #save method of Saveable module, which opens up additional possibilities like overriding the default behavior.

class XMLDocument
  include Saveable
  
  def save
    puts @data.inspect
    super
  end
end   

But why super ? To understand that, let's look at what super is.  

By definition,

super: Calls the current method in a superclass

When we included Saveable into XMLDocument, Saveable became a super from which behavior is inherited. This becomes even more evident when we inspect the ancestors of XMLDocument.

:0> XMLDocument.ancestors
=> [XMLDocument, Saveable, Object, Kernel, BasicObject]

We can see that sum-total of XMLDocument is the final inheritance or rather a mixin from itself as well as from a bunch of other classes and modules.

There are few other ways to share code between classes like Multiple Inheritance and Traits. Ruby doesn't support Multiple Inheritance.

Quoting Matz,

Single inheritance is good because the whole class inheritance structure forms a single tree with a single root, named Object, and that is very easy to understand. In languages with multiple inheritance, the classes form a network, which is harder to understand.

reference: https://web.archive.org/web/20191025214218/https://www.artima.com/intv/tuesday.html

Now that we have a clear idea of what include is, how to use it, and why is it designed that way, let's move on to the next way to use modules.

Enhancing the behavior of a Class

The way we can enhance the behavior of a Class is through the use of extend keyword.

module FileReadable
  def read(file_path)
    # reads the file at file_path
  end
end

class XMLDocument
  extend FileReadable
end

:0> xml_document = XMLDocument.read('/path/to/file.xml')

When we extended XMLDocument with FileReadable, functions on FileReadable became class methods of XMLDocument.

Even though extend lends a module's behavior to a class, ( as opposed to include which lends the behavior to an instance) the way extend works internally is very similar to include keyword.

But, let's digress to explore the subject of class or static methods in Ruby.

class XMLDocument
  def self.parse(raw_data)
  end
end

:0> XMLDocument.singleton_class.instance_methods.grep /parse/
=> [:parse]

In essence, every class method of XMLDocument is in reality, instance methods of XMLDocument's singleton or eigen or metaclass.

From Wikipedia,

A metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass defines the behavior of certain classes and their instances.

So far, we know the following:

  • An eigen class defines the behavior of a class
  • Class methods are instance methods on the eigen-class of a class
  • Include keyword added instance methods by pushing Saveable module up the ancestor chain or XMLDocument
:0> XMLDocument.singleton_class.ancestors
=> [#<Class:XMLDocument>, FileReadable, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]

In conclusion, extend keywork added instance methods to the metaclass of XMLDocument by pushing FileReadable module into its ancestor chain.

Decorating the existing behavior of an instance.

As its name suggests, prepend keyword prepends the behavior of a module before the instance's.

In one of our previous examples I have an XMLDocument with save capabilities, if we, for example, want to measure how many seconds it would take for save operation to complete.

module Saveable
  def save
  end
end

class XMLDocument
  include Saveable
end

module Timer
  def save
    t1 = Time.now
    super
    puts "Time taken to save: #{Time.now - t1}s"
  end
end

XMLDocument.prepend(Timer)

:0> xml_document = XMLDocument.new
:0> xml_document.save
Time taken to save: 0.084587s
=> nil

By just looking at the code, you my esteemed reader, might have guessed exactly what is going on under the hood. Let's inspect the ancestors chain again.

:0> XMLDocument.ancestors
=> [Timer, XMLDocument, Saveable, Object, Kernel, BasicObject]

Classes with just class methods

In our careers, we might have come across a fully functional class that has just class methods. A prime example is the quintessential kitchen-sink, the Util class.

class Util
  def self.do_something
  end
  
  def self.do_something_entirely_different
  end
end

This is an anti-pattern and is declining rapidly, thanks to RuboCop. This can be readily replaced by.

module Util
  module_function
  
  def do_something
  end
  
  def do_something_entirely_different
  end
end

Now that we know the many different direct use cases of modules, let us look at namespacing.

A way to logically bundle things.

Any programmer knows that "Fruits::Apple" must be something completely different from "CellPhone::Apple", we also know that "CellPhone::Apple" might be similar to "CellPhone::Nokia", hence we conclude with little-evidence that Namespacing has nothing to do with names and much more to do with similarity.

I won't be going much into namespaces and constant resolution, due to the simple fact that, the topic in itself is vast and can't beat these follwoing writeups as these posts discusses namespaces and constant resolution in great depth.

As a transitive container to hold code

To understand this a bit more, let us look into the ways in which a module can be defined in Ruby code.

module Sidekiq
end

and the above form is very much equivalent to the following code.

Sidekiq = Module.new do
end

Even though these are two forms are same for all intents and purposes, there is a subtle difference. The second form, takes an anonymous module and assigns it to a constant, while in first form there is no anonymous module involved due to the implicit assignment of the module code to the constant.

An example of anonymous module in action is class_methods from ActiveSupport::Concern. I am sure you, my reader, is extremely inquisitive and would love to find out how that works.

the tl;dr version.

  • The block of code passed to classmethods is assigned to an anonymous module
  • This anonymous module is then used to append class methods to the including class using extend

I have an explanation of how it works in one of my previous blogs, ( and the link points to just that section ).

We have exhausted all the possible ways we can use modules in Ruby. Let us learn a bit more, and read the fantastic history of modules.

A Brief history of module system

When Matz designed Ruby, he was heavily inspired by SmallTalk which is often considered as the perfect Object Oriented language. Ruby inherited a lot of features from SmallTalk, including MetaClass ( references in above section, Enhancing the behavior of a Class), to help maintain the "everything is an object" paradigm.

Even though SmallTalk is an Object Oriented language, from the perspective of its original designers, Alan Kay, Dan Ingalls, Adele Goldberg, and others at Xerox PARC, SmallTalk was designed as a functional language based on Lisp and Simula of which Simula supported features like Objects and Classes. One can argue that, while Lisp gave functional programming capabilities to SmallTalk, Simula gave it the "object-orientedness", like Classes and Objects.

But where does the module system come from? To answer this question, we need to go further back another generation. The immediate generation of The PASCAL Family of Programming Languages.  This family contains heavyweights like Ada, Simula, Eiffel and a lesser known Modula. It is Modula that brought forth modularization, the idea that "lets one define a public interface to hide the private implementation details".

Show Comments