Defining Nested Modules in Ruby

Nested Modules in Ruby

Ruby provides two different ways to define nested modules. This post explains the difference between them and how to decide which one to use. It also explains the `Module.nesting` method which returns the list of modules nested when called.

3 min read

Modules are a very powerful feature of Ruby. Primarily, they are used for two reasons: namespacing your code and sharing common methods across different classes (as mix-ins).

Using modules, you can create classes (or modules) with the same name without interfering with other classes. This allows you to design modular programs, breaking large components into smaller ones and also mixing and matching object behaviors.

# this module holds all the code
# related to tagging items. 
module Taggable

end

# module as namespace
module Blog
  class Post
    include Taggable  # module as mix-in
  end
  
  class Comment
    include Taggable  # module as mix-in
  end
end

You can also nest multiple modules to create nested namespaces. There are two ways to define nested modules (or classes) in Ruby.

  • Define the second module inside the first one. If module A doesn't exist, Ruby will create it first, then define module B. If module A exists, then it will re-open it and define module B inside it.
module A
  module B
  end
end
  • Use the :: operator to define the second module on the same line.
module A::B
end

There are three ways in which the second version (A::B) differs from the first:

  • It assumes that the outer module, i.e. A is already defined. If it's not, Ruby will throw a NameError: uninitialized constant A  error. So use this syntax only if you're sure that module A exists.
  • It allows you to reduce the amount of indentation. Instead of 3 levels of indentation, only one is necessary.
  • You can not access the constants defined inside module A inside module B without the scope resolution operator. This is a significant difference; the next section covers it in detail.

Accessing Constants in Nested Modules

The scope of constant access is different depending on how the nested module is defined. Let's consider the first version.

module A
  Z = 10
  
  module B
    puts Z
  end
end

# output 
10

You can define both modules at two separate locations, and it should still work.

module A
  Z = 10
end
 
# somewhere else, after the above module is defined
module A
  module B
    puts Z
  end
end
 
# output
10

Let's try accessing the constant in a module defined using the second syntax. It throws a NameError.

module A
  Z = 10
end

module A::B
  puts Z
end

# output
uninitialized constant A::B::Z (NameError)

However, you can access it using the scope resolution operator (::) as follows:

module A
  Z = 10
end

module A::B
  puts A::Z
end

# output
10
Trying to access a constant defined in the parent module without  :: operator fails with the second syntax.

The following example explains this point using classes and methods.

module A
  PI = 3.14
  
  module B
    def get_pi = PI  # method shorthand for one-liner methods
  end
end

class C
  include A::B
end

assert_equal 3.14, C.new.get_pi  # Passes!

However, if you use :: to define A::B without nesting it inside A, a NameError exception will be raised.

module A
  PI = 3.14
end

module A::B
  def get_pi = PI
end

class C
  include A::B
end

assert_raises(NameError) { C.new.get_pi }  # uninitialized constant A::B::PI (NameError)

The 'Module.nesting' Method

The nesting method defined on the Module returns the list of modules nested at the time of call.

The following example shows how the two versions differ in their module nesting.

module A
  p Module.nesting  # [A]
  
  module B
    p Module.nesting  # [A::B, A]
  end

  module B::C
    p Module.nesting  # [A::B::C, A]
  end
end

module A::D
  p Module.nesting  # [A::D]
end

When module D is defined, the nesting does not include module A. Hence, module D cannot access the constants defined in module A, without explicitly using the scope resolution (::) operator.


I hope you found this article useful and that you learned something new.

If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.