在Ruby中更改Proc的绑定

Sho*_* Ya 17 ruby

我有这个代码:

 l = lambda { a }
 def some_function
     a = 1
 end
Run Code Online (Sandbox Code Playgroud)

我只想a通过lambda和一个特殊的范围进行访问,该范围a已经some_function在示例中已经定义过,或者很快就会在以下相同的范围内:

 l = lambda { a }
 a = 1
 l.call
Run Code Online (Sandbox Code Playgroud)

然后我发现在调用时l,它仍然使用自己的绑定,但不是调用它的新绑定.

然后我试着用它作为:

 l.instance_eval do
     a = 1
     call
 end
Run Code Online (Sandbox Code Playgroud)

但这也失败了,奇怪的是我无法解释原因.

我知道其中一个解决方案是使用eval,其中我可以特别绑定并在文本中执行一些代码,但我真的不想这样使用.

而且,我知道它能够使用全局变量或实例变量.但是,实际上我的代码是在更深层次的嵌入式环境中,所以如果不是非常必要,我不想打破已完成的部分.

Proc在文档中引用了这个类,我发现了一个binding引用了Proc上下文的函数名.虽然该函数仅提供了一种访问其绑定但无法更改它的方法,除了使用Binding#eval.它也评估文本,这正是我不喜欢做的.

现在的问题是,我有更好(或更优雅)的方式来实现这个吗?或者使用eval已经是常规方式?

编辑回复@Andrew:
好的,这是我在写一个词法解析器时遇到的一个问题,我在其中定义了一个包含固定数量项的数组,其中包括至少一个Proc和一个正则表达式.我的目的是匹配正则表达式并在我的特殊范围内执行Procs,其中Proce将涉及一些应在稍后定义的局部变量.然后我遇到了上面的问题.
其实我想这是不完全一样对这个问题,因为我的是如何通过结合到一个Proc,而不是如何把它传递出去.

@Niklas:得到你的答案,我认为这正是我想要的.它完美地解决了我的问题.

Nik*_* B. 25

您可以尝试以下hack:

class Proc
  def call_with_vars(vars, *args)
    Struct.new(*vars.keys).new(*vars.values).instance_exec(*args, &self)
  end
end
Run Code Online (Sandbox Code Playgroud)

要像这样使用:

irb(main):001:0* lambda { foo }.call_with_vars(:foo => 3)
=> 3
irb(main):002:0> lambda { |a| foo + a }.call_with_vars({:foo => 3}, 1)
=> 4
Run Code Online (Sandbox Code Playgroud)

不过,这不是一个非常普遍的解决方案.如果我们可以给它Binding实例而不是哈希并且执行以下操作会更好:

l = lambda { |a| foo + a }
foo = 3
l.call_with_binding(binding, 1)  # => 4
Run Code Online (Sandbox Code Playgroud)

使用以下更复杂的hack,可以实现这种确切的行为:

class LookupStack
  def initialize(bindings = [])
    @bindings = bindings
  end

  def method_missing(m, *args)
    @bindings.reverse_each do |bind|
      begin
        method = eval("method(%s)" % m.inspect, bind)
      rescue NameError
      else
        return method.call(*args)
      end
      begin
        value = eval(m.to_s, bind)
        return value
      rescue NameError
      end
    end
    raise NoMethodError
  end

  def push_binding(bind)
    @bindings.push bind
  end

  def push_instance(obj)
    @bindings.push obj.instance_eval { binding }
  end

  def push_hash(vars)
    push_instance Struct.new(*vars.keys).new(*vars.values)
  end

  def run_proc(p, *args)
    instance_exec(*args, &p)
  end
end

class Proc
  def call_with_binding(bind, *args)
    LookupStack.new([bind]).run_proc(self, *args)
  end
end
Run Code Online (Sandbox Code Playgroud)

基本上我们定义了一个手动名称查找堆栈和instance_exec我们对它的proc.这是一种非常灵活的机制.它不仅可以实现call_with_binding,还可以用于构建更复杂的查找链:

l = lambda { |a| local + func(2) + some_method(1) + var + a }

local = 1
def func(x) x end

class Foo < Struct.new(:add)
  def some_method(x) x + add end
end

stack = LookupStack.new
stack.push_binding(binding)
stack.push_instance(Foo.new(2))
stack.push_hash(:var => 4)

p stack.run_proc(l, 5)
Run Code Online (Sandbox Code Playgroud)

打印15,如预期:)

更新:Github现在也提供代码.我现在也将它用于我的一个项目.