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

暫時寫到這裏。

相關文章
相關標籤/搜索