Ruby:将proc转换为lambda?

16

如何将proc风格的Proc转换为lambda风格的Proc?

有点惊讶,至少在1.9.2中无法实现:

my_proc = proc {|x| x}
my_lambda = lambda &p
my_lambda.lambda? # => false!
5个回答

22
这个有点棘手。在查看1.9的Proc#lambda?文档时,有一个关于proclamdba之间差异的相当冗长的讨论。
归根结底,一个lambda会强制执行正确数量的参数,而一个proc不会。从那份文件中得出的唯一将proc转换为lambda的方法显示在以下示例中:

define_method总是定义一个没有诡计的方法,即使给定了一个非lambda Proc对象。这是唯一的例外,其中不保留技巧。

 class C
   define_method(:e, &proc {})
 end
 C.new.e(1,2)       => ArgumentError
 C.new.method(:e).to_proc.lambda?   => true
如果想要避免污染任何类,你可以在匿名对象上定义一个单例方法,以便将 proc 强制转换为 lambda
def convert_to_lambda &block
  obj = Object.new
  obj.define_singleton_method(:_, &block)
  return obj.method(:_).to_proc
end

p = Proc.new {}
puts p.lambda? # false
puts(convert_to_lambda(&p).lambda?) # true

puts(convert_to_lambda(&(lambda {})).lambda?) # true

谢谢!非常有帮助 :) define_method 最终生成 lambda 是导致我困惑的原因。 - Alan O'Donnell
3
有趣的问题时间:如何在 jruby 中实现这个? - Schneems
1
我的有趣问题的答案:https://dev59.com/u2rWa4cB1Zd3GeqP_XvI - Schneems
如果您有兴趣使其成为一个稳定的接口。您能否请在此问题上发表评论,说明您最初为什么想要将proc转换为lambda?https://bugs.ruby-lang.org/issues/9777#change-46353 - Schneems

7

将proc转换为lambda是不可能的,因为这会引起麻烦。Mark Rushakoff的答案没有保留块中的self值,因为self变成了Object.new。Pawel Tomulik的答案无法在Ruby 2.1中工作,因为define_singleton_method现在返回一个符号,所以to_lambda2返回:_.to_proc

我的答案也是错误的:

def convert_to_lambda &block
  obj = block.binding.eval('self')
  Module.new.module_exec do
    define_method(:_, &block)
    instance_method(:_).bind(obj).to_proc
  end
end

它在块中保留了self的值:
p = 42.instance_exec { proc { self }}
puts p.lambda?      # false
puts p.call         # 42

q = convert_to_lambda &p
puts q.lambda?      # true
puts q.call         # 42

但是在使用 instance_exec 时会失败:

puts 66.instance_exec &p    # 66
puts 66.instance_exec &q    # 42, should be 66

我必须使用block.binding.eval('self')才能找到正确的对象。我将我的方法放在一个匿名模块中,这样它就不会污染任何类。然后我将我的方法绑定到正确的对象上。即使对象没有包含该模块,这也是有效的!绑定的方法会创建一个lambda。

66.instance_exec &q失败了,因为q实际上是绑定到42的方法,而instance_exec无法重新绑定该方法。可以通过扩展q以公开未绑定的方法,并重新定义instance_exec以将未绑定的方法绑定到不同的对象上来修复此问题。即使如此,module_execclass_exec仍将失败。

class Array
  $p = proc { def greet; puts "Hi!"; end }
end
$q = convert_to_lambda &$p
Hash.class_exec &$q
{}.greet # undefined method `greet' for {}:Hash (NoMethodError)

问题在于Hash.class_exec &$q定义了Array#greet而不是Hash#greet。(虽然$q是匿名模块的一个方法,但它仍然在Array中定义方法,而不是在匿名模块中。)使用原始的proc,Hash.class_exec &$p将定义Hash#greet。我得出结论:convert_to_lambda是错误的,因为它无法与class_exec一起使用。

5

以下是可能的解决方案:

class Proc
  def to_lambda
    return self if lambda?

    # Save local reference to self so we can use it in module_exec/lambda scopes
    source_proc = self

    # Convert proc to unbound method
    unbound_method = Module.new.module_exec do
      instance_method( define_method( :_proc_call, &source_proc ))
    end

    # Return lambda which binds our unbound method to correct receiver and calls it with given args/block
    lambda do |*args, &block|
      # If binding doesn't changed (eg. lambda_obj.call) then bind method to original proc binding,
      # otherwise bind to current binding (eg. instance_exec(&lambda_obj)).
      unbound_method.bind( self == source_proc ? source_proc.receiver : self ).call( *args, &block )
    end
  end

  def receiver
    binding.eval( "self" )
  end
end

p1 = Proc.new { puts "self = #{self.inspect}" }
l1 = p1.to_lambda

p1.call #=> self = main
l1.call #=> self = main

p1.call( 42 ) #=> self = main
l1.call( 42 ) #=> ArgumentError: wrong number of arguments (1 for 0)

42.instance_exec( &p1 ) #=> self = 42
42.instance_exec( &l1 ) #=> self = 42

p2 = Proc.new { return "foo" }
l2 = p2.to_lambda

p2.call #=> LocalJumpError: unexpected return
l2.call #=> "foo"

应该在Ruby 2.1及以上版本上运行


3

0

以上代码与 instance_exec 不兼容,但我认为简单的修复程序可以解决这个问题。在这里,我有一个例子来说明问题和解决方案:

# /tmp/test.rb
def to_lambda1(&block)
  obj = Object.new
  obj.define_singleton_method(:_,&block)
  obj.method(:_).to_proc
end

def to_lambda2(&block)
  Object.new.define_singleton_method(:_,&block).to_proc
end


l1 = to_lambda1 do
  print "to_lambda1: #{self.class.name}\n"
end
print "l1.lambda?: #{l1.lambda?}\n"

l2 = to_lambda2 do
  print "to_lambda2: #{self.class.name}\n"
end
print "l2.lambda?: #{l2.lambda?}\n"

class A; end

A.new.instance_exec &l1
A.new.instance_exec &l2

to_lambda1基本上是由Mark提出的实现,to_lambda2是一个“修复”的代码。

以上脚本的输出为:

l1.lambda?: true
l2.lambda?: true
to_lambda1: Object
to_lambda2: A

实际上,我期望instance_exec输出A而不是Objectinstance_exec应该改变绑定)。我不知道为什么这个工作方式不同,但我认为define_singleton_method返回的方法尚未绑定到Object,而Object#method返回的是已经绑定的方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接