define_method in a class method

2.3k views Asked by At

In the following code, the module is extended which means the method hash_initialized is treated as a class method, or an instance method of the eigen class. This is what we need since hash_initialized is called in the context of the eigen class.

What i don't understand is if the context for the module is the eigen class, then define_method should create a instance method called "initialize" of the eigen class, or in other words, a class method of class Cheese. Don't we need an instance method "initialize" here?

module HashInitialized
  def hash_initialized(*fields)
    define_method(:initialize) do |h|
        missing = fields - h.keys
      raise Exception, "Not all fields set: #{missing}" if missing.any?

      h.each do |k,v|
        instance_variable_set("@#{k}", v) if fields.include?(k) 
      end
    end
  end
end

class Cheese
  extend HashInitialized
  attr_accessor :color, :odor, :taste
  hash_initialized :color, :odor, :taste
end
3

There are 3 answers

0
Cary Swoveland On BEST ANSWER

When you encounter seeming conundrums such as this one, try salting your code with puts self statements:

module HashInitialized
  puts "self when parsed=#{self}"
  def hash_initialized(*fields)
    puts "self within hash_initialized=#{self}"
    define_method(:initialize) do |h|
      missing = fields - h.keys
      raise ArgumentError, "Not all fields set: #{missing}" if missing.any?
      fields.each { |k| instance_variable_set("@#{k}", h[k]) } 
    end
    private :initialize
  end
end
  #-> self when parsed=HashInitialized

class Cheese
  extend HashInitialized
  attr_accessor :color, :odor, :taste
  hash_initialized :color, :odor, :taste
end
  #-> self within hash_initialized=Cheese

As you see, self is the class Cheese, not Cheese's singleton_class. Hence, the receiver for Module#define_method is Cheese, so the method obligingly creates the instance method initialize on Cheese.

Cheese.instance_methods(false)
  #=> [:color, :color=, :odor, :odor=, :taste, :taste=] 

initialize is not among the instance methods created on Cheese because I modified the code slightly to make it a private method:

Cheese.private_instance_methods(false)
  #=> [:initialize]

I also slightly altered the code that assigns values to the instance variables, and made the type of exception more specific.

If appropriate, you could change your argument test to:

raise ArgumentError, "Fields #{fields} and keys #{h.keys} don't match" if
  (fields-h.keys).any? || (h.keys-fields).any?

You may wish to have initialize create the assessors:

module HashInitialized
  def hash_initialized(*fields)
    define_method(:initialize) do |h|
      missing = fields - h.keys
      raise ArgumentError, "Not all fields set: #{missing}" if missing.any?
      fields.each do |k|
        instance_variable_set("@#{k}", h[k])
        self.class.singleton_class.send(:attr_accessor, k)
      end
    end
    private :initialize
  end
end

class Cheese
  extend HashInitialized
  hash_initialized :color, :odor, :taste
end

Cheese.new :color=>'blue', odor: 'whew!', taste: "wow!"
  => #<Cheese:0x007f97fa07d2a0 @color="blue", @odor="whew!", @taste="wow!"> 
1
Jim Deville On

Calling extend technically puts the module in the lookup chain of the eigen object it is called on, which in this case is the same as the class object. So you are correct that the context for hash_initialized is the class. Additionally, you are correct that the context for define_method is the class. However, your final step is incorrect. When define_method is called in that context it defines an instance method, not a singleton method.

IOW, when you call define_method in a context, it defines the method in the same place that def would define it at that context.

0
yuzhoul On

I've made this clear by simplifying the example above and added a few printouts.

class Test
  def self.define_something
    define_method(:inside_class_method){puts "method defined inside a class method"}
    puts "self inside class method "+self.to_s
    proc = Proc.new{puts "method defined using send inside class method"}
    self.send(:define_method, :define_using_send_inside_class_method, proc)
  end

  class << self
    puts "self inside eigen class "+self.to_s
  end

  def test
    puts "self inside of instance method "+self.to_s
  end
  puts "self outside of class method "+self.to_s
  define_method(:outside_class_method){puts "method defined outside a class method"}
  define_something
end

Test.new().inside_class_method
Test.new().outside_class_method
Test.new().test
Test.define_using_send_inside_class_method

This code produce the following outputs:

self inside eigen class #

self outside of class method Test

self inside class method Test

method defined inside a class method

method defined outside a class method

self inside of instance method #

test.rb:26:in <main>': undefined methoddefine_using_send_inside_class_method' for Test:Class (NoMethodError)

This code:

self.send(:define_method, :define_using_send_inside_class_method, proc)

It is also defining an instance method since it is called on self, and self is referring to the class Test.

If we need to define a class method, send needs to be called on the eigen class like this:

class << self
  self.send(:define_method, :define_using_send_inside_class_method, proc)
end