Ruby lambda binding 的一些行为

cicholgricenchos · 2014年12月12日 · 最后由 cicholgricenchos 回复于 2014年12月12日 · 2985 次阅读

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 只能接收字符串代码,要获得某对象的引用必须知道它的名字,然而在这里没办法获取这个名字,期望在创建的时候设置名字也不现实,所以用这种方法传递了对象的引用。

共收到 10 条回复

以前上课的时候学到的知识是,绑定总是从内而外查找绑定目标。如果最外层也没有可以绑定的目标,就会出错。

以前上课的时候学到的知识是,绑定总是由内而外查找绑定目标。如果最外层也没有可以绑定的目标,就会出错。

哈哈,推荐这本书,配图讲的比较清楚,我看完后比以前明白了一点。

  1. closure(包括 block, proc, lambda) 语法作用域是绑定死的,动态的部分只有参数表和 self(通过 instance_eval 等改变)。
  2. 执行一个 proc 的时候,先检查 lexical_scope_chain 每个本地变量表,如果没有再检查当前 self 的方法表。
  3. 创建 proc 时,只会到绑定当时的变量表(会把当时的变量表复制到堆上,再引用),所以之后定义的变量都看不到;但是方法表是通过 self 在运行时找的,所以之后定义的方法可以看到。
  4. binding 只能取得 proc 的语法作用域,不能修改
b = lambda{ p local_variables }
a = lambda { |b|
  c = 1
  (lambda &b).call
}
p a.call(b) 
#=>等价于
p b.call
#=>等价于
p self.send(:local_variables) 
a = lambda{ p b } #这里只绑定了此时此刻的变量表(链),所以它永远看不到b变量,所以会编译成self.send(:b)
b = 1
a.call    #这里才执行self.send(:b),但是当前self没有b方法
a = lambda{ p b } # 这里编译时先找变量b找不到,所以会编译成执行self.send(:b)
def b
  1
end
a.call # => 1   #这里才执行self.send(:b),所以没问题

如果只是想复用一些代码,可以使用 UnboundMethod

module A
  def test(a)
    puts a + 1
  end
end

un_test = A.instance_method(:test)

class B
end
new_test1 = un_test.bind(B)
new_test1.call(5)

另外关于为什么 eval 不能定义局部变量

locals are bound at compile-time since 1.9

#2 楼 @msg7086 和这个应该没关系啦

#3 楼 @davidhuangdw 还存在这个编译的过程啊。。总觉得很反直觉,在 js python 用那种写法都是正常的

#4 楼 @serco 这个也很有意思,把方法放到 module 里然后就可以直接绑给任一个类了。。

#3 楼 @davidhuangdw 在解释器读取到 Proc 代码的时候进行编译,而编译的过程参照了当时的本地变量表,所以编译之后再增加的变量是读取不到的,这样理解对吧?

#8 楼 @cicholgricenchos 呵呵,应该是的,我也不是太确定。。。

运行到定义 proc 代码的时候,把当时的本地变量表从 stack 复制一份到 heap 上,然后让这个 proc 引用它。所以后边再声明的变量都不会加到这个 heap 复制表(而是加到 stack 上),所以新加的变量都找不到。

然后,应该是直接编译成类似 self.send(:b) 了,而不是运行时才查询变量表链,这样效率挺高的。

#9 楼 @davidhuangdw 嗯,我去下本 ruby under a microscope 看看【【

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册