ruby

Ruby - How to include classes in a Module


Im new in Ruby. I have seen that Modules in Ruby are used for namespacing or for mixin.

I would like to use a module for namespacing. Module will include class definitions.

This has been my attempt.

lib/HtmlBody.rb

module HtmlBody
    require_relative './html_body/HeadingTags'
    require_relative './html_body/AnchorTags'
    require_relative './html_body/ImgTags'
end

lib/html_body/HeadingTags.rb

class HeadingTags
  ...
end

And from another file, I would require the module lib/HtmlBody.

require_relative 'lib/HtmlBody'

HtmlBody::HeadingTags.new

This will return an error. :

1: from (irb):9:in `rescue in irb_binding'
NameError (uninitialized constant HtmlBody::HeadingTags)

Im not sure what the issue is. I understand that it says uninitialized but Im not sure why. It seems it is looking for a constant instead of reading the class?

How are you supposed to include classes located in separated files inside a module?

The is something that Im missing in Ruby and the require/require_relative probably.


Solution

  • Im not sure what the issue is. I understand that it says uninitialized but Im not sure why. It seems it is looking for a constant instead of reading the class?

    It is not clear to me what you mean by "reading the class". Yes, Ruby is looking for a constant. Variable names that begin with a capital letter are constants, ergo, HtmlBody is a constant, HeadingTags is a constant, and HtmlBody::HeadingTags is the constant HeadingTags located in a class or module that is referenced by the constant HtmlBody.

    How are you supposed to include classes located in separated files inside a module?

    You namespace a class inside a module by defining the class inside the module. If you are sure that the module already exists, you can define the class like this:

    class HtmlBody::HeadingTags
      # …
    end
    

    However, if HtmlBody is not defined (or is not a class or module), this will fail.

    module HtmlBody
      class HeadingTags
        # …
      end
    end
    

    This will guarantee that module HtmlBody will be created if it doesn't exist (and simply re-opened if it already exists).

    There is also a slight difference in constant lookup rules between the two, which is however not relevant to your question (but be aware of it).

    The is something that Im missing in Ruby and the require/require_relative probably.

    Indeed, your question stems from a fundamental misunderstanding of what Kerne#load / Kernel#require / Kernel#require_relative does.

    Here is the very complicated, detailed, in-depth explanation of all the incredibly convoluted stuff that those three methods do. Brace yourself! Are you ready? Here we go:

    They run the file.

    Wait … that's it? Yes, that's it! That's all there is to it. They run the file.

    So, what happens when you run a file that looks like this:

    class HeadingTags
      # …
    end
    

    It defines a class named HeadingTags in the top-level namespace, right?

    Okay, so what happens when we now do this:

    require_relative './html_body/HeadingTags'
    

    Well, we said that require_relative simply runs the file. And we said that running that file defines a class named HeadingTags in the top-level namespace. Therefore, this will obviously define a class named HeadingTags in the top-level namespace.

    Now, looking at your code: what happens, when we do this:

    module HtmlBody
      require_relative './html_body/HeadingTags'
    end
    

    Again, we said that require_relative simply runs the file. Nothing more. Nothing less. Just run the file. And what did we say running that file does? It defines a class named HeadingTags in the top-level namespace.

    So, what will calling require_relative from within the module definition of HtmlBody do? It will define a class named HeadingTags in the top-level namespace. Because require_relative simply runs the file, and thus the result will be exactly the same as running the file, and the result of running file is that it defines the class in the top-level namespace.

    So, how do you actually achieve what you are trying to do? Well, if you want to define a class inside a module, you have to … define the class inside the module!

    lib/html_body.rb

    require_relative 'html_body/heading_tags'
    require_relative 'html_body/anchor_tags'
    require_relative 'html_body/img_tags'
    
    module HtmlBody; end
    

    lib/html_body/heading_tags.rb

    module HtmlBody
      class HeadingTags
        # …
      end
    end
    

    lib/html_body/anchor_tags.rb

    module HtmlBody
      class AnchorTags
        # …
      end
    end
    

    lib/html_body/img_tags.rb

    module HtmlBody
      class ImgTags
        # …
      end
    end
    

    main.rb

    require_relative 'lib/html_body'
    
    HtmlBody::HeadingTags.new