0

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?

3
  • 1
    Why would you think that you are missing something? Is there an error message? If yes, what is it? Is the output not what you are expecting? If yes, what is the output, and what is the output you are expecting? Is the behavior not what you are expecting? If yes, what is the behavior, and what is the behavior you are expecting? Is nothing happening? Is something happening that shouldn't happen? Is something not happening that should happen? Please provide a minimal, self-contained test-case, the inputs and expected outputs and behaviors of said test-case, and any and all errors and warnings. Commented Oct 15, 2015 at 14:12
  • I think I'm missing something cuz I'm not getting the expected behaviour. Aw I just updated the error I'm getting. Any clue? Commented Oct 15, 2015 at 14:16
  • seems the self here related to Country class and not the instance of Country. How should I fix this? Commented Oct 15, 2015 at 14:17

3 Answers 3

3

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!"

Sign up to request clarification or add additional context in comments.

Comments

2

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

1 Comment

Wow, Awesome. Thanks @Jean Bob
1

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 

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.