Reputation: 6017
I have some code something like :
class Country
attr_reader :name
def initialize
@name = "MyName".freeze
end
def government
@government ||= Government.new(self)
end
def symbols
@symbols ||= Symbols.new(self)
end
def economy
@economy ||= Economy.new(self)
end
def education
@education ||= Education.new(self)
end
def healthcare
@healthcare ||= Healthcare.new(self)
end
def holidays
@holidays ||= Holidays.new(self)
end
def religion
@religion ||= Religion.new(self)
end
end
How can I create the methods dynamically? I tried :
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
define_method(m) do |argument|
instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self))
end
end
def initialize
@name = "MyName".freeze
end
end
If I try:
puts Country.new.education.inspect
I get the following error:
country.rb:16:in `block (2 levels) in <class:Country>': wrong number of arguments (0 for 1) (ArgumentError)
from country.rb:27:in `<main>'
What am I missing here?
Upvotes: 0
Views: 1510
Reputation: 369428
In your original code, you defined all the methods to take no arguments:
def education
# ^^^
@education ||= Education.new(self)
end
In the metaprogrammed code, you define all the methods to take a single argument called argument
:
define_method(m) do |argument|
# ^^^^^^^^^^
instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end
However, you call it with zero arguments:
puts Country.new.education.inspect
# ^^^
Obviously, your methods are meant to be lazy getters, so they should take no arguments:
define_method(m) do
instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end
Note that there are other problems with your code. In your original code, you use a conditional assignment to only perform the assignment if the instance variable is undefined, nil
or false
, whereas in the metaprogrammed code, you are always unconditionally setting it. It should be something more like this:
define_method(m) do
if instance_variable_defined?(:"@#{m}")
instance_variable_get(:"@#{m}")
else
instance_variable_set(:"@#{m}", const_get(m.capitalize).new(self))
end
end
Note: I also removed the Object.
from the call to const_get
to look up the constant using the normal constant lookup rules (i.e. first lexically outwards then upwards in the inheritance hierarchy), since this corresponds to how you look up the constants in the original code snippet.
This is not fully equivalent to your code, since it sets the instance variable only when it is undefined and not also when it is false
or nil
, but I guess that is closer to your intentions anyway.
I would encapsulate this code to make its intentions clearer:
class Module
def lazy_attr_reader(name, default=(no_default = true), &block)
define_method(name) do
if instance_variable_defined?(:"@#{name}")
instance_variable_get(:"@#{name}")
else
instance_variable_set(:"@#{name}",
if no_default then block.(name) else default end)
end
end
end
end
class Country
attr_reader :name
COMPONENTS = %w(government symbols economy education healthcare holidays religion)
COMPONENTS.each do |m|
lazy_attr_reader(m) do |name|
const_get(name.capitalize).new(self))
end
end
def initialize
@name = 'MyName'.freeze
end
end
That way, someone reading your Country
class won't go "Huh, so there is this loop which defines methods which sometimes get and sometimes set instance variables", but instead think "Ah, this is a loop which creates lazy getters!"
Upvotes: 3
Reputation: 565
You can simply use eval :
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
eval <<-DEFINE_METHOD
def #{m}
@#{m} ||= #{m.capitalize}.new(self)
end
DEFINE_METHOD
end
def initialize
@name = "MyName".freeze
end
end
Upvotes: 2
Reputation: 984
I guess you don't need the argument
:
class Country
attr_reader :name
COMPONENETS = %w(government symbols economy education healthcare holidays religion)
COMPONENETS.each do |m|
define_method(m) do
instance_variable_set("@#{m}",Object.const_get(m.capitalize).new(self))
end
end
def initialize
@name = "MyName".freeze
end
end
Upvotes: 1