kitimenpolku
kitimenpolku

Reputation: 2604

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.

Upvotes: 0

Views: 2000

Answers (4)

engineersmnky
engineersmnky

Reputation: 29598

I am not suggesting this is a good idea but ruby is always willing to give you enough rope to shoot yourself in the foot so...

Just for fun you can hack this functionality together (with extremely generic assumptions made) as:

module HtmlBody
  def self.include_in_scope(constant_name,path)
    require_relative path
    self.const_set(constant_name.to_s, Object.send(:remove_const, constant_name.to_s))
  end
end

klass_list = {HeadingTags: './html_body/HeadingTags', 
              AnchorTags: './html_body/AnchorTags'
              ImgTags: './html_body/ImgTags'}

klass_list.each do |name,path|
   HtmlBody.include_in_scope(name, path) 
end

Now all the classes are namespaced under HtmlBody e.g. HtmlBody::HeadingTags but are not included in the top level scope e.g. ::HeadingTags will raise a NameError.

Upvotes: 0

Josh Brody
Josh Brody

Reputation: 5363

Jorg's answer is much more thorough and goes through the why. I'll defer to his, but leave my succinct answer in place. https://stackoverflow.com/a/58034705/1937435

You can accomplish what you're trying to do with eval, but it's not recommended. The functionality you're looking for here is accomplished with include and extend.

include will assign a module's methods on the instance-level. extend will assign a module's methods on the class-level.

module HtmlBody; end

module HeadingInstanceMethods
  def h1
    puts "I am h1"
  end
end

module HeadingClassMethods
  def valid_headers
    ["h1", "h2"]
  end
end

module HtmlBody
  class HeadingTags
    include HeadingInstanceMethods
    extend HeadingClassMethods
  end
end 

HtmlBody::HeadingTags.valid_headers # => ["h1", "h2"]
HtmlBody::HeadingTags.new.h1 # => I am h1

When you splice these into separate files, just do the usual require or require_relative at the top of the file (not in the namespace) and call them accordingly.

Upvotes: 2

Jörg W Mittag
Jörg W Mittag

Reputation: 369624

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

Upvotes: 7

Stefan Rendevski
Stefan Rendevski

Reputation: 301

You are getting the error Uninitialized Constant because, in ruby, HeadingTags and HtmlBody::HeadingTags are two different constants. Ruby does not take into consideration the file path here. To achieve what you want, you need to declare HeadingTags as belonging to HtmlBody explicitly, like this:

htmlbody.rb

module HtmlBody; end

htmlbody/headingtags.rb

module HtmlBody
    class HeadingTags; end
end

Or alternatively class HtmlBody::HeadingTags; end. However, if you want to dynamically define constants under a module, you can look into the const_set method.

Upvotes: 3

Related Questions