
[Day19]「吃下惡魔果實,什麼 Block 都能變成物件!」
不得不說尾田老師真的是常常腦洞大開,居然還有吃了狗狗果實的槍還有吃了象象果實的劍!
前幾天提到了 Block 不能在程式裡單獨存在,不過,今天要介紹的 Proc 和 Lambda ,在某些地方又被稱為「匿名函數」,它們可以幫忙把 Block 拿出來變成物件使用。
簡單來說,Proc 是一種「物件化」的 Block,如果稍微忘記物件的朋友可以先坐時光機回到 第 10 天 再回來繼續看喔!
物件化?具現化?
從物件導向的角度切入,當我們需要 Block 可以被傳遞,並作為一個 receiver 來接受訊息&使用方法,這時候就會用 Proc 對 Block 進行物件化,我們直接看程式碼吧!
一般的方法只能被呼叫:
def add_two()
    2 + n
end
add_two(3)
但如果透過 Proc.new 產生一個 Proc 物件,就可以呼叫其他方法了:
add_two = Proc.new { |x| x * 2 }
add_two.call(3)
除了呼叫方法之外,也能在程式裡被傳來傳去了!變成了 Proc 以後真是好處多多啊!
list = [1, 2, 3, 4, 5]
double = Proc.new { |x| x * 2 }
p list.map { |element| double.call(element) }
# 印出
[2, 4, 6, 8, 10]
Proc 不小心忘了寫 new好!現在我們已經知道要用 Proc.new 把一個 Block 物件化:
double = Proc.new { x * 2 }
# 印出
#<Proc:0x00007fa4592659a8> 
有時候可能會忘了 new ,不小心寫成 proc:
double = proc { x * 2 }
# 印出
#<Proc:0x00007fa459284e48> 
別擔心這樣也可以!
接著要介紹一個和 Proc 很像的東西:Lambda ,它的寫法是這樣:
double = -> { x * 2 }
#<Proc:0x00007fa4598f5de0 (lambda)> 
如果 Javascript 寫習慣了想用 -> 也行:
double = -> { x * 2 }
# 印出
#<Proc:0x00007fa45a0acac0 (lambda)> 
Ruby 還會貼心提醒你這是個 lambda :)
Lambda 和 Proc 有什麼不同?兩個真的很像吧?Lambda 的效果就和 Proc 一樣,都能把 Block 變成一個獨立的物件,但兩者還是有差異的,否則幹嘛要設計兩個名詞自找麻煩呢?
Lambda 和 Proc 的差異主要有二:
Lambda 和 Proc 回傳值的方式不同這邊借五倍的範例來做解說:
def double(callable_object)
  callable_object.call * 2
end
la = lambda { return 10 }
pr = proc { return 10 }
double(la) 
=> 20
puts double(pr) 
=> LocalJumpError (unexpected return)
從以上結果發現 Lambda 可以被當作帶入方法的參數,而 Proc 就無法辦到,但這並不是 Proc 不能執行或沒有回傳值,而是因為 Proc 在回傳值時,是從定義 Proc 時的 scope 回傳。
嗯...什麼意思?
再繼續看這個:
def lambda_double
  la = lambda { return 10 }
  result = la.call
  return result * 2
end
def proc_double
  pr = Proc.new { return 10 }
  result = pr.call
  return result * 2  # unreachable code!
end
現在把 Lambda 和 Proc 放在方法裡,我預期 lambda_double 和 proc_double 應該都會得到 20 這個結果,但事實上:
lambda_double
=> 20
puts proc_double 
=> 10
我們從這個例子可以更明顯地看到 Lambda 和 Proc 的不同,Lambda 的回傳值可以離開 Lambda 本身給其他程式碼使用,但 Proc 就只能在一開始定義這個 Proc 的那行回傳。
Lambda 和 Proc 參數判斷的方式不同如果我們給 Proc 的參數數量不對:
pr = proc {|a,b| [a,b]}
pr.call('a', 'b')
=> ["a", "b"]
pr.call('a')
=> ["a", nil]
pr.call('a', 'b', 'c')
=> ["a", "b"]
可以發現,如果參數給的比預期多 Proc 會自動忽略;如果比預期少的話,則會補一個 nil,基本上都還是可以動,這點和 JavaScript 蠻像的!
相對來說,Lambda 在參數數量這點上就比較嚴格,數量必須要給正確才行:
la = lambda {|a,b| [a,b]}
p la.call('a', 'b')
=> ["a", "b"]
p la.call('a')
=> ArgumentError (wrong number of arguments (given 1, expected 2))
p la.call('a', 'b', 'c')
=> ArgumentError (wrong number of arguments (given 3, expected 2))
&?這是什麼?最後來看一個比較特殊的情形,先直接看 code:
list1 = [1, 2, 3, 4, 5]
list2 = ["a", "b", "c"]
double = -> (x) { x * 2 }
p list1.map(&double)
# 印出
[2, 4, 6, 8, 10]
咦咦咦!這什麼?
第一次看到時,差點以為要被毀三觀,更冒出一堆黑人問號:
map 後面不是要加 Block 嗎?double 在這裡不是一個 Lambda 物件嗎?& 又是什麼?在經過一番消化理解後,才發現原來在這裡是把 double 這個Lambda 物件前面如果加上了 &,就能又轉成了 Block 接在方法後面使用(寫法卻已經大大不同了!)真的是翻了很多文章才看出一點端倪(盯)
今天翻閱參考文章時,還有看到一個很特別的用 & + symbol 的寫法!我花點時間弄清楚後會再貼上來分享給大家!
在 Javascript 的世界裡,function 具有 higher order 的特性,因此可以被當作參數丟來丟去,不過在 Ruby 的世界裡就沒有此特性,必須先寫成 Proc 或 Lambda 才能達成,不過其實還好,因為 Ruby 方法已經很好用了,每個語言本就有各自的強項和弱項,這和程式語言的設計哲學有很大的關聯。
呼~今天這篇真的是花很多時間在寫,都沒時間找梗圖了!希望自己對 Proc 還有 Lambda 的理解還算正確,如有說明不清楚的地方,歡迎大家留言在下方或給我一些寫作上的建議,感謝!