How You Nest Modules Matters in Ruby

3 min read
ruby

Ruby provides two different syntaxes to nest modules (and classes):

# Syntax #1
module API
  module V1
  end
end

# Syntax #2
module API::V1
end

Syntax #2 requires that API module already exists. Most Rubyists know that. Besides this difference, they think that these two syntaxes are interchangeable. With that line of thinking, what syntax to use for nesting modules turns out to be a matter of preference.

But, the syntax you choose matters, which I will demonstrate shortly with examples that relate to how you would organize a versioned REST API.

Modules are just constants in Ruby, as such, regular constant look-up rules apply. The very first rule relates to how modules are nested. For the purpose of this blog post, we will ignore regular constants and just focus on modules.

When you reference a constant in a nested context, Ruby looks it up inside out:

module API
  class Responder
  end

  module V1
    class Controller
      def action
        Responder.respond_with('Hello, World!')
      end
    end
  end
end

When #action gets called, Ruby will see if Responder is defined inside API::V1::Controller first, API::V1 second, API third, and finally in the top level. If it wasn't for this inside-out look-up, referencing Responder as above would not have worked. Instead, it would have to be referenced as API::Responder.

You can actually access the nesting information Ruby uses for this look-up with Module.nesting:

module API
  class Responder
  end

  module V1
    class Controller
      p Module.nesting #=> [API::V1::Controller, API::V1, API]

      def action
        Responder.respond_with('Hello, World!')
      end
    end
  end
end

Now, let's see how the look-up behavior changes when we mix in syntax #2:

module API
  class Responder
  end
end

module API::V1
  class Controller
    p Module.nesting #=> [API::V1::Controller, API::V1]

    def action
      Responder.respond_with('Hello, World!')
    end
  end
end

When #action gets called, you will now get a NameError:

NameError: uninitialized constant API::V1::Controller::Responder

This is because some nesting information gets lost when you use syntax #2. You can see this from what Module.nesting returns in the example above. As per that example, Ruby now looks for Responder inside API::V1::Controller and API::V1. It no longer looks inside API, where Responder is actually defined hence the error.

Conclusion

The syntax you use for nesting modules in Ruby should not be a matter of preference. The syntax you use could be the difference between a working program, and the one that throws an error. So choose wisely. Having said that, I prefer syntax #1.

By the way, we've explored just one aspect of how constant look-up works in Ruby in general. If you use Rails, the matters gets even more complicated with the autoloading magic. The following references will help you learn more: