Ruby中的Proc/lambda

原创
2015/12/07 18:08
阅读数 357

Ruby中的Proc,有两种,一种是 Proc 一种是 Lambda,可以通过 lambda? 来检测是否为lambda。其实lambda就是proc的另外一种形态:

 >  ->{}  # 创建一个lambda
 => #<Proc:0x007fc3fb809e60@(irb):46 (lambda)>  # 可以看到返回的是Proc对象
>  ->{}.class 
=> Proc # 确实是Proc对象

Proc 和 Lambda 和区别是,Proc相当于代码植入,而Lambda是函数调用,所以Proc可以翻译为代码块,而Lambda,就叫匿名函数好了。
lambda因为是匿名函数,所以会在参数不对时候抛出异常,而proc就没有这样的问题,其次在proc内return会导致整个调用立即返回,后面的代码不再执行,而lambda则不会,所以永远不应该在proc中写return语句。


Proc创建方法有两种:

Proc.new{|x| x + 1 }
proc{|x| x + 1 }

可以查查是不是真的proc、还是lambda:

>  Proc.new{}.lambda?
=> false 
>  proc{}.lambda?
=> false

注:在Ruby1.8中,Kernel#proc() 其实是 Kernel#lambda()的别名,因为遭到大量Rubyists反对,所以在 Ruby1.9版本中,Kernel#proc()变成了 Proc.new()的别名了。

Lambda也有两种

lambda{|x| x + 1 }
-> x { x + 1 }

检查是否为 lambda:

>  lambda{}.lambda?
=> true
>  ->{}.lambda?
=> true

Proc和Lambda都有以下四中调用方式:

pc = Proc.new{|x| x + 1 } 
pc.call(x)
pc[x]
pc.(x) # Ruby1.9 增加
pc===x # Ruby1.9 增加

对于最后一种 === 三个等号,资料非常少,单个参数调用proc/lambda均没有问题,但当有多个参数时:

>  proc{|x,y| x+y } === 1,2
=> SyntaxError: (irb):189: syntax error, unexpected ',', expecting end-of-input

发现,无法调用,语法解析出错了。

>  proc{|x,y| x+y } === [1,2]
=> 3

调用成功!因为proc支持传递数组,并将数组自动展开,所以可以正常调用。
但当是lambda时

>  lambda{|x,y| x+y } === 1, 2
=> SyntaxError: (irb):191: syntax error, unexpected ',', expecting end-of-input
>  lambda{|x,y| x+y } === [1,2]
=> ArgumentError: wrong number of arguments (1 for 2)

根本无法调用,因为lambda需要检测参数个数,并且不会将数组展开,这里的简便写法不正确。
查看 rubinius源代码 :

alias_method :===, :call

可以看到,三个等号就是 call的别名,这个方法没什么特别的, 🐹尝试这样写:

>  lambda{|x,y| x+y}.=== 1, 2
=> 3

这里加了个点号,当然也可以写成:

>  lambda{|x,y| x+y}.===(1, 2)
=> 3

这里的等号和括号直接不能有空格,要是写成 .=== (1,2) 就会出错。
所以,推荐使用一下三种方式调用 prod/lambda,仅这一个参数的时候才用 ===,三等号只是让你写DSL的时候看起来更清爽一点,尽量少用。

pc.call(x,y)
pc[x,y]
pc.(x,y)

stackoverflow上有人问,为何proc/lambda调用一定要注明 .call / .() / [] 这样,为何不能省略,就像方法那样,直接调用?
比如 pc ,那是因为ruby调用函数、方法时,可以省略圆括号,(), 这样解析器就无法区分到底是在传递proc,还是在调用他!


Ruby1.9开始 lambda支持参数默认值

>  ->(x, y=2){ x+y }.(1)
=> 3


send 方法

Ruby的Object,都有一个send方法,可以通过他传递方法名称,实现动态调用方法,
比如:相加

>  1.send(:+, 2) 
=> 3

取子字符串

>  "hello".send :[], 0,1
=> h

相当于 "hello"[0,1]
但除了send还有一个一模一样的 __send__ 方法,为何会有两个名字?因为最早是只有send的,但考虑到很多场景比如socket会使用send来发送数据包,用户也可能会自定义send方法,这样就无法调用发送原先的send方法,所以又出来一个__send__来,这个方法不应该被override的。

Ruby的map本来只接收proc,但有时候却可以这样写:%w(a b c).map(&:upcase)

>  %w(a b c).map &:upcase
=> ["A", "B", "C"]

&符号表示后面传递的变量其实是一个proc,所以等价成这样:

>  %w(a b c).map &:upcase.to_proc
=> ["A", "B", "C"]

当ruby发现后面跟着的不是proc对象后,将会调用该对象的to_proc方法,实现将其转换成proc对象:

Proc.new{|obj| obj.send :upcase}

如果没用定义to_proc,那么调用失败。
之所以map后面可以传入一个Symbol对象,是因为Ruby实现了Symbol对象的to_proc方法,这种用法最早出现在Rails里,
在Ruby1.8.7里面原生实现了Symbol对象的to_proc方法。

查看Rubinius里的实现:

class Symbol
  def to_proc
    # Put sym in the outer enclosure so that this proc can be instance_eval'd.
    # If we used self in the block and the block is passed to instance_eval, then
    # self becomes the object instance_eval was called on. So to get around this,
    # we leave the symbol in sym and use it in the block.
    #
    sym = self
    Proc.new do |*args, &b|
      raise ArgumentError, "no receiver given" if args.empty?
      args.shift.__send__(sym, *args, &b)
    end
  end
end

可以看到,为了避免send方法被复写,rubinius里使用的是__send__方法。
来实现一个最简单的to_my_proc方法

class Symbol
    def to_my_proc
        Proc.new{|obj| obj.send self}
    end
end

测试:

>  %w(a b c).map(&:upcase.to_my_proc)
=> ["A", "B", "C"]

调用成功了,对比这个简单的to_my_proc和Rubinius实现的差别,第一,proc只接收一个参数,忽略了其余的参数,
而rubinius则将其余的参数也当作方法的调用参数,一并send给了obj调用,第二个差别是self在外面单独赋值一次,
避免调用instance_eval的时候被覆盖,第三个差别是,同时传递了可能传递的block参数。

但隐式调用to_proc,这种方式,是没办法传递更多的参数的,比如要实现以下字符串解析成hash
'a:b;c:d' 转成 hash: {a=>b, c=>d}
基本写法:

>  'a:b;c:d'.split(';').map{|s| s.split ':' }.to_h
=> {"a"=>"b", "c"=>"d"}

使用to_proc简写:
第一步,先split成将要转成hash的数组

>  'a:b;c:d'.split(';').map &:split
=> [["a:b"], ["c:d"]]

split默认的参数是空格,所以转换失败。
添加参数试试:

>  'a:b;c:d'.split(';').map &:split(':')
=>  SyntaxError: (irb):187: syntax error, unexpected '(', expecting end-of-input
=>  'a:b;c:d'.split(';').map &:split(':')
                                    ^

语法错误❌,这样写不行,因为:split(':')并不是合法的Symbol对象。
其实,通过to_proc的源代码也能看出来,默认的to_proc方法不接受更多的参数。(因为定义 to_proc 后面根本没有根参数,其实跟了也无法传递,因为是隐式调用)
所以,改造自己的to_my_proc方法,让其接收更多的参数,然后显式调用:

class Symbol
  def to_my_proc(*args)
    Proc.new{|obj| obj.send self, *args }
  end
end
>  'a:b;c:d'.split(';').map(&:split.to_my_proc(':')).to_h
=> {"a"=>"b", "c"=>"d"}

调用成功。
如果把方法名称定义为call而不是to_my_proc,则可以通过.()来调用,测试:

class TestCall
    def call(*args)
        puts "called: #{args}"
    end
end
>  TestCall.new.call "hello", "world"
=> call method called: ["hello", "world"]
>  TestCall.new.("hello", "world")
=> call method called: ["hello", "world"]

同样调用成功,证明ruby内部实现了.() 来表示.call 的别名,但是暂时没有找到实现的源代码。
所以把to_my_proc写成call:

class Symbol
  def call(*args)
    Proc.new{|obj| obj.send self, *args }
  end
end
>  'a:b;c:d'.split(';').map(&:split.(':')).to_h
=> {"a"=>"b", "c"=>"d"}

调用成功,即使将该方法定义成 to_proc也不能隐式调用,因为后面的不是合法的Symbol对象,语法报错,所以虽然是支持了参数,但却必须显式调用该方法,返回一个proc对象供map调用。 这个to_my_proc/call写的非常简单,仅仅是传递了最基本的参数而已。stackoverflow上有写的更完善的代码:

class Symbol
  def call(*args, &block)
    ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
  end
end

这里用的是lambda,换成proc一样。
这样的方法在考虑到有多个参数传递到proc的时候,比如 each_with_index{|e, i| } 这样的情况,还有比如
[1,2,3].reduce(0){|r, e| r += e } ,这时候调用的proc会传递多个参数(放到*rest里),但可能结果不是想要的!比如实际调用可能会解析成:->(e, i) { e.send :方法, i, 其他参数 },相当于将剩余的参数也一并传递给了第一个参数:caller的方法调用了,所以在block内有多个参数时候,不建议用简写!

既然任何类只要实现了to_proc方法就能在前面加&符号,直接转换为proc调用,那么如果定义了数组的to_proc,同样可以,
所以,stackoverflow上有人想出了这样的代码:

class Array
  def to_proc
    Proc.new{|obj| obj.send *self }
  end
end

这样就可以直接传递数组给map调用了,这里的*self就代表数组展开,测试代码:

>  'a:b;c:d'.split(';').map(&[:split, ':']).to_h
=> {"a"=>"b", "c"=>"d"}

估计用的人多了,Ruby可能会考虑内置这个方法,这个写法比Symbol的to_proc要更合适,尤其在需要传递参数给方法时,这种写法比起 &:方法.(参数) 看起来更直观。

在看inject/reduce方法,这个方法可以有以下同样有效的写法:

[1,2,3].reduce(0, &:+)
[1,2,3].reduce(&:+)
[1,2,3].reduce(:+)
[1,2,3].reduce('+')

第一种写法没有问题,第二个参数作为proc传递,调用Symbol的to_proc方法将其转换成 result.send :+, item 方法调用,
第二个其实也没有太大疑问,因为缺少初始值,实际内部循环只执行2次,第一次,1,不执行,第二次,用1作为初始值,执行带入block {|result, item| result + item } 得到最终值,
第三种和第四种,在map里如果直接写,会提示出错,而这里就能正常,原因是reduce其实可以接收两个参数(而map是不接收参数的),当没有传递block时,会尝试将参数转换成symbol,然后调用symbol的to_proc方法将其转换成proc调用,所以等价成里第二种。
Rubinius实现代码:

def inject(initial=undefined, sym=undefined)
    if !block_given? or !undefined.equal?(sym) # 在没用block、或者有两个参数时!
      if undefined.equal?(sym) # 在只有一个参数的情况下,这一个参数必须为可以转换成方法调用,比如 :+。
        sym = initial  
        initial = undefined
      end
      # Do the sym version
      sym = sym.to_sym # 指定了to_sym方法的对象,都可以传递,所以可以传递 :+ 或者 '+'
      each do
        o = Rubinius.single_block_arg
        if undefined.equal? initial # 如果没初始值,将初始值置为第一个参数值,不执行sym转换成的proc代码
          initial = o
        else # 否则正常执行sym转换的proc代码
          initial = initial.__send__(sym, o)
        end
      end
      # Block version
    else # 当有block参数,并且只有一个参数调用的情况
      each do
        o = Rubinius.single_block_arg
        if undefined.equal? initial # 没用传递初始值时候,将第一个值为初始值,不执行proc体代码
          initial = o
        else # 将初始值带入proc执行。
          initial = yield(initial, o)
        end
      end
    end
    undefined.equal?(initial) ? nil : initial
end

因为上面我自己定义的Symbol的call方法并没用考虑 block后多个参数的情况,即inject/reduce的代码块:
[1,2,3].inject{|r, e| r + e },这种情况,所以如果尝试调用call会失败,稍微改造以下,让Symbol的call接收更多的参数:

class Symbol
    def call(*args)
        Proc.new{|first, *others| first.send self, *others, *args}
    end
end

至于为什么可以出现两个带*的参数,因为这不是函数定义,是函数调用,所有的带*参数会依次展开传入send方法调用,所以不会出现歧义。
调用:

>  [1,2,3].inject &:+.()
=> 6

这里一定要加&符号,表明带入时proc调用,而非单个参数,因为如果是单个参数一定要可以转换为Symbol对象,然后再通过Symbol的to_proc实现调用,而 :+.() 显然不是合法的Symbol对象,而是对象调用call方法的简写,上面有说明。

同样,Array的to_proc方法也需要改造

class Array
  def to_proc
    sym = self.shift
    Proc.new{|first, *others| first.send sym, *others, *self}
  end
end
> [1,2,3].inject &[:+]
=> 6

实际这样没什么用。

Ruby 2.3版本中, 新增了 Hash 的 to_proc方法:

>  h = { foo:1, bar: 2, baz: 3} 
>  p = h.to_proc
>  p.call :foo # 相当于 h[:foo] 
=> 1

单纯这样没有什么用,但是可以用在map里,获取所有的值:

>  [:foo, :bar].map { |key| h[key] } # h访问的是上面定义的h 
=> [1, 2]

这样就可以简写为:

>  [:foo, :bar].map &h 
=> [1, 2]

可以自己实现一个简单的 Hash to_proc

Class Hash
    def to_proc
        Proc.new{ |key| self[key] }
    end
end

暂时写到这里。



展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部