Proc 在创建的时候就会和周边的上下文绑定,如果想复用,就必须改变它的 binding。在对象内有 instance_eval 之类的方法可用,但是在对象外就没办法了,于是我想研究一下怎么在对象外复用 lambda。
b = lambda{ p local_variables }
a = lambda { |b|
c = 1
(lambda &b).call
}
a.call(b) # => [:b, :a] 没有:c
我的考虑是在 a 内重新创建一个和 b 一样的 lambda,于是将 b 展开,并重新用 lambda 语句创建(这里用 Proc.new 结果也相同),按道理这个新建的 lambda 应当有 a 内的 binding,但事实上是 b 的 binding 被保留了。
打印这个新 lambda 和 b 的 object_id,发现是相同的。这样似乎不符合直觉,因为既然 b 展开了,行为应该和代码块是一样的才对,而且语句表达的应该是新建一个 lambda,为什么成了拷贝呢?
a = lambda { |&b|
c = 1
(lambda &b).call
}
a.call { p local_variables } # => [:a]
于是我试了真的传个代码块进去,结果还是不行,检查了之后发现这个块在传进 lambda 的时候已经变成了一个 Proc 对象,而它的 binding 还是在外面,也算讲得通吧。
可是重新创建都不行,就没办法修改 lambda 的 binding 了,为了让它在其他地方可用,只能够用 eval 给它的 binding 注入一些需要的信息。期间我观察了一下 binding 的特性,有些出乎意料的地方。
注意这一段,行为很诡异
a = lambda{ p b }
b = 1
a.call # => NameError 找不到b
a 的 binding 应该就是 main,但却获取不到 b,难道 binding 还会保存变量的状态?
b = 1
a = lambda{ p b }
b = 2
a.call # => 2
a = lambda{ p b }
eval "b = 1", a.binding
a.call # => NameError 找不到b
a = lambda{ p b }
eval "b = 1", a.binding
p b # => NameError 找不到b
b = 1
a = lambda{ p b }
eval "b = 2", a.binding
a.call # => 2
如果 main 原先存在 b,就可以正确地获取到,而且值也可以更新,所以 binding 保存的不是变量的状态,而是域内存不存在这个变量。如果 lambda 创建时上下文不存在 b,即使通过 eval 在其 binding 执行了 b 的定义,这个定义也不会被带出 eval,不会为 main 带来变化,难道 eval 本身又生成了一个闭包?
a = lambda{ p b }
def b
1
end
a.call # => 1
不过方法调用没有此限制,用 eval 给 lambda 注入方法是可行的。
我觉得 eval 应该允许块作为参数,不然在对象外对块进行复用会十分困难,既然 instance_eval 这些可以存在,实现上应该也不会很难才对。
====== 补充
#b = 1
a = lambda { p b }
puts RubyVM::InstructionSequence.disasm(a)
用 RubyVM::InstructionSequence.disasm 可以查看 lambda 编译后的代码,上图是 lambda 定义前不存在 b,下图是存在 b,可以看到上图调用了方法 b,而下图是个 getlocal,所以两个 lambda 本质上有不同
====== 再次补充
我原先是想做一个模仿 js 的对象和原型继承的程序,使用纯 lambda,现在已经做出来了:
https://github.com/CicholGricenchos/tricks/blob/master/prototype/using_pure_lambda.rb
其中遇到最大的问题就是 lambda 作用域的问题,不过如果 lambda 的 binding 不能改变,可以用一个全局变量向里面传递需要的参数,也就近似于改变了上下文。程序中对象的成员 lambda 几乎都是在外部生成的,没办法操作对象本身,于是我用 eval 及一个全局变量向这些 lambda 的作用域注入了一个指向对象的 this,这样它们的行为和 js 的函数应该就相同了。
其实也可以不用全局变量,但是 eval 只能接收字符串代码,要获得某对象的引用必须知道它的名字,然而在这里没办法获取这个名字,期望在创建的时候设置名字也不现实,所以用这种方法传递了对象的引用。