Niels Kristian
Niels Kristian

Reputation: 8825

Why is my define_method content being evaluated in the class scope?

I'm doing some (too) fancy meta programming, and I have a hard time understanding why the scope is different in the following two cases:

Case 1:

class TesterA

  def the_method
    puts "I'm an instance_method!"
  end

  def self.the_method
    puts "I'm a class_method!"
  end

  def self.define_my_methods *method_names
    method_names.each do |name|
      define_method("method_#{name}") do
        the_method
      end
    end
  end

  define_my_methods :a, :b, :c
end

t = TesterA.new
t.method_a #=> I'm an instance_method!
t.method_b #=> I'm an instance_method!
t.method_c #=> I'm an instance_method!

Case 2

class TesterB

  def the_method
    puts "I'm an instance_method!"
  end

  def self.the_method
    puts "I'm a class_method!"
  end

  def self.define_the_method attr
    define_method("method_#{attr}") do
      begin
        yield
      rescue
        raise $!, "method_#{attr} was called: #$!", $@
      end
    end
  end

  def self.define_my_methods *method_names
    method_names.each do |name|
      define_the_method(name) do
        the_method
      end
    end
  end

  define_my_methods :a, :b, :c
end

t = TesterB.new
t.method_a #=> I'm a class_method!
t.method_b #=> I'm a class_method!
t.method_c #=> I'm a class_method!

I the second example I introduce a kind of "helper-mothod" define_the_method which I use for defining the methods rather than define_method it self. Reason for that is, That I want to append the name of the dynamic method to any exception messages that might occur inside those methods. The problem however is, that the content (when using the latter case) seems to be evaluated in the class-scope.

Why is this, and how can I make it get evaluated in the instance-scope?

Upvotes: 3

Views: 201

Answers (3)

potashin
potashin

Reputation: 44581

It happens because you are providing the block in the scope of the class method self.define_my_methods, where self is a class, not an instance. So what you can do is yield the scope of the define_method itself:

def self.define_the_method attr
  define_method("method_#{attr}") do
    begin
      yield self
    rescue
      raise $!, "method_#{attr} was called: #$!", $@
    end
  end
end

def self.define_my_methods *method_names
  method_names.each do |name|
    define_the_method(name) do |scope|
      scope.send(:the_method)
    end
  end
end

Upvotes: 4

Danil Speransky
Danil Speransky

Reputation: 30453

That's because proc defined with define_method will be called by using instance_eval.

In the first sample it is:

do
  the_method
end

In second:

do
  begin
    yield
  rescue
    raise $!, "method_#{attr} was called: #$!", $@
  end
end

But yield will have parent scope from the place it was defined.

Here is my suggestion:

def self.define_the_method attr, &block
  define_method("method_#{attr}") do
    begin
      instance_eval(&block)
    rescue
      raise $!, "method_#{attr} was called: #$!", $@
    end
  end
end

Upvotes: 2

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

This block:

do
  the_method
end

it defined and scoped in class-scope. There is no reason to expect it’ll be somehow injected into instance-scope. To fix the issue simply pass the receiver explicitly:

yield(self)

and:

def self.define_my_methods *method_names
  method_names.each do |name|
    define_the_method(name) do |receiver|
      receiver.the_method
    end
  end
end

Upvotes: 4

Related Questions