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
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
暫時寫到這裏。