Reputation: 2604
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
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
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
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
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