Ruby lambda binding 的一些行为

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

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看看【【

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