Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I call a Proc that takes a block in a different context?

Take this example Proc:

proc = Proc.new {|x,y,&block| block.call(x,y,self.instance_method)}

It takes two arguments, x and y, and also a block.

I want to execute that block using different values for self. Something like this nearly works:

some_object.instance_exec("x arg", "y arg", &proc)

However that doesn't allow you to pass in a block. This also doesn't work

some_object.instance_exec("x arg", "y arg", another_proc, &proc)

nor does

some_object.instance_exec("x arg", "y arg", &another_proc, &proc)

I'm not sure what else could work here. Is this possible, and if so how do you do it?

Edit: Basically if you can get this rspec file to pass by changing the change_scope_of_proc method, you have solved my problem.

require 'rspec'

class SomeClass
  def instance_method(x)
    "Hello #{x}"
  end
end

class AnotherClass
  def instance_method(x)
    "Goodbye #{x}"
  end

  def make_proc
    Proc.new do |x, &block|
      instance_method(block.call(x))
    end
  end
end

def change_scope_of_proc(new_self, proc)
  # TODO fix me!!!
  proc
end

describe "change_scope_of_proc" do
  it "should change the instance method that is called" do
    some_class = SomeClass.new
    another_class = AnotherClass.new
    proc = another_class.make_proc
    fixed_proc = change_scope_of_proc(some_class, proc)
    result = fixed_proc.call("Wor") do |x|
      "#{x}ld"
    end

    result.should == "Hello World"
  end
end
like image 882
David Miani Avatar asked Mar 26 '12 11:03

David Miani


1 Answers

To solve this, you need to re-bind the Proc to the new class.

Here's your solution, leveraging some good code from Rails core_ext:

require 'rspec'

# Same as original post

class SomeClass
  def instance_method(x)
    "Hello #{x}"
  end
end

# Same as original post

class AnotherClass
  def instance_method(x)
    "Goodbye #{x}"
  end

  def make_proc
    Proc.new do |x, &block|
      instance_method(block.call(x))
    end
  end
end

### SOLUTION ###

# From activesupport lib/active_support/core_ext/kernel/singleton_class.rb

module Kernel
  # Returns the object's singleton class.
  def singleton_class
    class << self
      self
    end
  end unless respond_to?(:singleton_class) # exists in 1.9.2

  # class_eval on an object acts like singleton_class.class_eval.
  def class_eval(*args, &block)
    singleton_class.class_eval(*args, &block)
  end
end

# From activesupport lib/active_support/core_ext/proc.rb 

class Proc #:nodoc:
  def bind(object)
    block, time = self, Time.now
    object.class_eval do
      method_name = "__bind_#{time.to_i}_#{time.usec}"
      define_method(method_name, &block)
      method = instance_method(method_name)
      remove_method(method_name)
      method
    end.bind(object)
  end
end

# Here's the method you requested

def change_scope_of_proc(new_self, proc)
  return proc.bind(new_self)
end

# Same as original post

describe "change_scope_of_proc" do
  it "should change the instance method that is called" do
    some_class = SomeClass.new
    another_class = AnotherClass.new
    proc = another_class.make_proc
    fixed_proc = change_scope_of_proc(some_class, proc)
    result = fixed_proc.call("Wor") do |x|
      "#{x}ld"
    end
    result.should == "Hello World"
  end
end
like image 136
joelparkerhenderson Avatar answered Oct 23 '22 17:10

joelparkerhenderson