初來乍到Ruby
世界的讀者們,絕對想不到原來Ruby
也有 curry
, bind
等用法。這些語法對於JS
的使用者應該很熟悉,在今年的IT鐵人賽就看到很多介紹bind
, curry
Closure
是程式語言中很基本的概念,首先要先講Proc
和 closure
的關係
在Ruby
的Block則是用{}
或do ... end
來做領域的展開,領域裡和領域外是完全不同的世界。外面的值沒有辦法干預到裡面,而裡頭的數值和運算也不會影響到外界。這就是closure
的概念,我們稱為閉包。
沒有術式就不會有領域,而以 Ruby
來說,一共有下列幾種術式能夠展開領域。
#====== Proc
proc {}
Proc.new {}
#====== Proc(Lambda)
lambda {}
-> {}
#====== map, each...
[1, 2, 3].each {}
我們也可以自己做術式,而我們使用的術式為yield
。
block_given?
為yield
的好朋友,所以我們先介紹block_given?
def apple_machine
block_given? && 'block_used' || 'block_unused'
end
apple_machine #=> block_unused
apple_machine {} #=> block_used
由上面的例子我們可以知道, block_given?
可以偵測apple_machine
是否使用block
, block_given?
可以讓我們在有block
或沒有block
的情況下做應對,接著我們開始講yield
。
# 情境1
def apple_machine
yield if block_given?
end
apple_machine { 1 } #=> 1
apple_machine { 1 + 1 } #=> 2
apple_machine { [1, 2, 3].sum } #=> 6
# 情境2
def washing_machine
yield if block_given?
2
end
apple_machine { 1 } #=> 2
apple_machine { 1 + 1 } #=> 2
apple_machine { [1, 2, 3].sum } #=> 2
yield
字面上的意思為讓路。上述的情境1、情境2,想要傳達給讀者的訊息為不管是簡單的1、稍微複雜的1+1,或者更複雜的[1, 2, 3].sum
。在進入block
以後,程式碼的會先被Block
拿走主控權,結束block
以後再回歸原來的程式。
# 情境3
def itday9_method(num = 0)
yield(num + 1) if block_given?
end
itday9_method #=> nil
itday9_method(2) { |n| n*100 } #=> 300
[1, 2, 3].map { |n| itday9_method(n) { |p| p*100 } } #=> [200, 300, 400]
我們可以利用yield
將方法裡頭的值傳出去給外面的Block
,給外面的Block
計算以後再回傳運算的結果回來。情境3的用法很重要
實際的狀況下,我們傳的不會只是num+1
,可以是綠界回傳的付款結果、貨運單狀態、POS錯誤訊息等等。
yield
或許不是個好寫法,因為使用太多的Block
會造成維護上的不易讀性,不過個人在專案上用的地方還蠻多的。使用在只有自己在維護的專案裡面,寫了Block
就可以少想很多設計流程。
以下有幾個例子,可以正大光明在Rails
使用Block
。
view
:如視窗畫面、卡片、頁籤layout
:搭配content_for
,如使用Action Mailer
decorator pattern
,可以用yield
實現 ➡️ Day32 提到攤提Bind 其實是 React 使用 class component 比較常見的寫法,但 bind 不是只能綁定 this
,還可以綁任何物件。下面的例子為,javascript
的bind
將裡面的this
與外面的[1, 2, 3]
綁在一起。
function Point() {
return [...this, 4, 5, 6]
}
point = Point.bind([1, 2, 3])
point()
// [1, 2, 3, 4, 5, 6]
bind
是一種可以和外部世界聯絡的橋樑,而這個方法在Ruby
也有。
a = 123 # 外部變數
block = proc { a } # Procedure
block.binding.local_variable_get(:a) # 綁定外部的變數
block.call #=> 123
Curry
是Javascript
對function
的一種進階用法,在JS
中也是漢漢老師常用的寫法。此外,我也常常在面試中看到使用Curry
的題目
/* 公司後輩的面試題目 */
function adder(a) {
/* function 可以當作變數,因此我們可以將 function 回傳出去。
意即為: function return 出來的結果還是 function */
return function(b) {
return a + b
}
}
/* 公司專案常見的寫法,寫法等同於上方 */
const adder = a => b => (a + b)
/* 範例1: a = 1, b = 2 */
adder(1)(2) // 3
/* 範例2: 用在map */
[1, 2, 3].map(e => adder(1)(e)) // [2, 3, 4]
範例1
用了兩個括號把值回傳出來,範例2為使用map
這個 Higher Order Function
,我們在使用Higher Order Function
時,裡面的adder
必須也是function
。利用curry的特性,做一個匿名函數 e => adder(1)(e)
提供給map
使用。
curry
絕對不是炫技用,而是為了做到把function
可以變成變數的特性而衍生出的技巧。以下為漢漢老師實際上使用的curry
實例
/* 實際應用上使用的 curry (看不懂沒關係) */
export const ajaxReload = (table) => () => table.api().ajax.reload();
export const multiAjaxReload = (tables) => () => tables.forEach(table => table.api().ajax.reload())
export const dataWithStatus = (selectedValue, type = null) => e => ({...e, [checkedOrSelected(type)]: is_in(e.value, selectedValue)})
Ruby
也有一樣的方法,我們來看以下例子
divisible_by = ->(x,y) { (y % x).zero? }.curry
(1..10).select(&divisible_by.call(5)) #=> [5, 10]
(1..10).select(&divisible_by.call(2)) #=> [2, 4, 6, 8, 10]
我們分別傳5, 2 給divisible_by
這個物件對,接著select
會使用divisible_by.call
作為篩選。至於 &
符號,我們會在Day10解釋
Day10 我們會介紹&
,以及詳細說明yield
的運作原理!