Oswaldo Ferreira
Oswaldo Ferreira

Reputation: 1339

Send message to object via class scope (metaprogramming)

I need a way to send a message from stuff method (via metaprogramming) which executes my_method on the object scope. There's a good way of doing that without inserting more code on Dummy class?

class Dummy
  stuff :final_value do
    value :my_method
  end

  def my_method
    10.5
  end

  def final_value
    0
  end
end

Expected return:

dummy = Dummy.new
dummy.final_value
=> 10.5

The idea is to take the methods on value args and find a way to map those values from the object scope, which is this example is 10.5.

Posting a temporary solution down there.

Upvotes: 0

Views: 361

Answers (2)

Oswaldo Ferreira
Oswaldo Ferreira

Reputation: 1339

A temporary way of dealing with it was executing code on define_method scope, which is the object scope itself. The solution is not fully tested but the idea is pretty much that:

def stuff(method_name, &block)
  subject = Stuff.new(block)

  # Defining the final_value method and executing code on object scope.

  define_method method_name do
    # making the sum on object scope (demand is a list of *value* args, 
    # which is a list of methods to execute on object scope)
    #
    # The logic here is to sum them to return as final_value result

    subject.total = subject.demand.map do |method|
      self.send(method)
    end.reduce(:+)

    subject.total
  end

  subject.evaluate!
end

class Stuff
  attr_accessor :demand, :total

  def initialize(stuff_block)
    @stuff_block = stuff_block
    @total = 0
  end

  # Brings all stuff block into Stuff class
  def evaluate!
    @stuff_block.call
  end

  # The main problem was this.
  # The solution was mapping those demanded methods into a demand local variable,
  # then accessing this variable on define_method scope inside *stuff* method, which is the
  # object scope itself. Bahn!
  def value(*args)
    @demand = args
  end
end

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110665

This should do it.

class Dummy
  def stuff
    self.class.send(:define_method, :final_value) do
      my_method
    end
  end

  def my_method
    10.5
  end

  def final_value
    0
  end
end

dummy = Dummy.new
dummy.stuff
dummy.final_value #=> 10.5

The purpose of the method stuff is to change the method final_value to:

def final_value
  my_method
end

so we need to do, dynamically, the equivalent of:

class Dummy
  def final_value
    my_method
  end
end

Here, when final_value is created, self is Dummy. We therefore need to instruct Dummy to define the method final_value as above, thereby replacing its earlier definition. Fortunately, there's a method that does just that: Module#define_method.

To execute :define_method, we need only send it, together with its argument :final_value and a block that is to be the body of :final_value, to Dummy, using the method Object#send:

def stuff
  Dummy.send(self.class.send(:define_method, :final_value) do
    my_method
  end
end

That works fine, but suppose we were to rename the class to, say, Smartie? We'd have to remember to change Dummy to Smartie in the method stuff. It's better to replace Dummy with self.class. By the way, this is one of the few instances where you do need to include self., as class alone is interpreted as the keyword class, as in class Dummy.

Edit: I may have misunderstood the question. (See comments below.) The following may be what you want:

class Dummy
  def self.stuff(method, &block)
    send(:define_method, method, &block)
  end

  def my_method
    10.5
  end

  def final_value
    0
  end

  stuff :final_value do
    my_method
  end
end

Dummy.new.final_value #=> 10.5

Note, because Ruby parses the lines sequentially, building the class as it goes,

stuff :final_value do
  my_method
end

must appear after the other methods.

Upvotes: 1

Related Questions