ruby life questions
ruby life questions

Reputation: 129

Using `instance_eval`

I am reading why's poignant guide to ruby, and in chapter 6, he used this code:

class Creature
  # Get a metaclass for this class
  def self.metaclass; class << self; self; end; end

  ...

  def self.traits( *arr )
    # 2. Add a new class method to for each trait.
    arr.each do |a|
      metaclass.instance_eval do
        define_method( a ) do |val|
          @traits ||= {}
          @traits[a] = val
        end
      end
    end
  end
end

Why is he calling instance_eval on metaclass of class Creature? Since instance_eval adds methods to metaclass, he can just do this:

def self.metaclass; self; end;

Or am I wrong? Are there any more elegant solutions to this?

Upvotes: 1

Views: 912

Answers (2)

Max
Max

Reputation: 22315

The simpler way to write _why's code would be just

def self.traits( *arr )
  # 2. Add a new class method to for each trait.
  arr.each do |a|
    metaclass.define_method(a) do |val|
      @traits ||= {}
      @traits[a] = val
    end
  end
end

The only problem here is that you get an error:

private method `define_method' called for metaclass (NoMethodError)

A private method can only be called with an implicit receiver i.e. the problem is the explicit metaclass. before the method call. But if we remove that, the implicit receiver (self) is Creature! So how do we change self to a different object? instance_eval:

metaclass.instance_eval do
  define_method(a) do |val|
    ...
  end
end

So it's really just a way of bypassing the fact that define_method is private. Another way to hack it would be to use send

metaclass.send(:define_method, a) do |val|
  ...
end

But these days all of that is totally unnecessary; you are allowed to define methods in the metaclass (AKA singleton class) without hacking around private methods:

def self.traits( *arr )
  # 2. Add a new class method to for each trait.
  arr.each do |a|
    define_singleton_method(a) do |val|
      @traits ||= {}
      @traits[a] = val
    end
  end
end

Upvotes: 3

intale
intale

Reputation: 831

If you do

def self.metaclass; self; end;

you will have a reference to Creature class. So, in that case, the methods will be defined not to the singleton class of object Creature but to the class Creature itself(to the list of instance methods of class Creature). The method

def self.metaclass; class << self; self; end; end

is a simple way to retrieve singleton class of object Creature in ruby < 1.9. In ruby 1.9+ was implemented method singleton_class which is shortcut of class << self. So that code can be simplified as:

class Creature

  ...

  def self.traits( *arr )
    # 2. Add a new class method to for each trait.
    arr.each do |a|
      singleton_class.instance_eval do
        define_method( a ) do |val|
          @traits ||= {}
          @traits[a] = val
        end
      end
    end
  end
end

Upvotes: 1

Related Questions