如何调用在不同上下文中占用块的Proc?

时间:2012-03-26 11:22:18

标签: ruby metaprogramming block

举个例子Proc:

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

它需要两个参数,x和y,以及一个块。

我想使用不同的self值来执行该块。这样的事情几乎可行:

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

然而,这不允许你传入一个块。这也行不通

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

也不是

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

我不确定这里还有什么可行的。这是可能的,如果是这样,你怎么做?

编辑:基本上如果您可以通过更改change_scope_of_proc方法来传递此rspec文件,那么您已解决了我的问题。

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

2 个答案:

答案 0 :(得分:8)

要解决此问题,您需要将Proc重新绑定到新类。

这是您的解决方案,利用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

答案 1 :(得分:-2)

我认为你不能做到这一点,麻烦不是传递多个块。 Proc和block是闭包,并在创建时捕获它们的绑定。 self是该绑定的一部分,因此即使您使用instance_eval更改自身,当您call在其绑定中执行proc / block时,它会自动关闭:

$ irb
irb(main):001:0> class Foo; def mkproc; Proc.new { puts "#{self.class}:#{object_id}" }; end; end
=> nil
irb(main):002:0> p = Foo.new.mkproc
=> #<Proc:0x00000001b04338@(irb):1>
irb(main):003:0> p.call
Foo:14164520
=> nil
irb(main):004:0> 'bar'.instance_exec { puts "#{self.class}:#{object_id}"; p.call }
String:16299940
Foo:14164520

Ruby将允许您使用Binding捕获闭包Kernel#binding,但无法设置与Proc关联的绑定。您可以为Kernel#eval的字符串版本指定绑定,但这仍然不允许您更改您调用的proc的绑定。

irb(main):005:0> class BindMe; def get_binding(p=nil); binding; end; end
=> nil
irb(main):006:0> b = BindMe.new.get_binding(p)
=> #<Binding:0x00000001f58e48>
irb(main):007:0> eval '"#{self.class}:#{object_id}"', b
=> "BindMe:14098300"
irb(main):008:0> eval '"#{self.class}:#{object_id}"', p.binding
=> "Foo:14164520"
irb(main):009:0> eval "p.call", b
Foo:14164520