Ruby中的Proc/lambda

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语句。ruby


Proc建立方法有两种:
socket

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

能够查查是否是真的proc、仍是lambda:
ide

>  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都有如下四中调用方式:
this

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

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

>  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时
rest

>  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

暂时写到这里。

相关文章
相关标签/搜索