Functional-Light JavaScript - Argument Adapters
前言
此為 frontend master 的 Functional-Light JavaScript, v3 的課程筆記,這篇主要講的是 argument adapters,在使用 function 時,不一定每個 function parameters 的順序、數量符合我們的需求,因此就需要透過 adapter 去調整,把 function 修改成符合我們需求的形狀。
內容不完全和課程編排相同,單純是個人消化過後的編排。
因為剛開始學 functional programming 的緣故,內容、觀念可能會有諸多錯誤,如有發現錯誤麻煩不吝指教,感謝!
Function Arguments
先來談談 parameter 和 argument 的差別,parameter 指的是 function 的參數,argument 指的是傳進去的數,舉例來說:
function add(x, y) {
return x + y
}
add(3, 4)
x
和 y
就是 parameter,3
、4
就是 arguments。
Ths shape of function
再來要講一件很重要的事情,就是 function 的 shape。
function 的 shape 指的是接收多少個 input 以及回傳多少 output,舉例來說:
// unary
function increment(x) {
return sum(x, 1)
}
// binary
function sum(x, y) {
return x + y
}
只需要傳進去一個 input,回傳一個 output 的叫做 unary;需要傳兩個 input,回傳一個 output 的叫做 binary,而三個以上的 input 就叫做 ternary。所以increment
就是 unary,sum
就是 binary。
Function 的形狀是很重要的,因為這代表了和其他 function 的契合程度、好不好共同使用,就像是小朋友在堆樂高積木一樣,下面的積木必須要和上面的積木形狀是契合的,才能夠組成一座高塔,如果積木不合就沒辦法合在一起使用了,function 也是如此,shape 決定了 function 一起使用的方便性。
當我們在設計 function 的時候就必須考量到 shape,而一般來說,好的 FP program 通常大部分的 function 都會是 unary 的,少部分是 binary,ternary 就特別少。
往後其他章節提到的例子,就會看到如果 function 的 shape 不合,會遭遇什麼不方便,以及我們如何用其他技巧克服這問題喔。
Arguments Shape Adapters
有時候我們想要使用的 function 不一定是我們期望的 shape,比如我們想要 unary 的,function 卻是 binary 的,這時候就要透過一個叫 adapter 的方法來去處理這件事情。
什麼是 adapter 呢?以設計模式來說,有一個叫做 Adapter Pattern(適配器模式)的東西,概念上就是把兩個形狀不合的東西,透過一個 Adapter 橋接兩者,讓兩個形狀不同的東西可以合在一起使用,是個類似於轉接頭的概念,所以我們會用 adapter 當作轉接頭橋接兩個 shape 不合的 function 。
adapter 基本上會是 HOF(Higher Order Function),那甚麼叫 HOF(Higher Order Function)呢?
Higher Order Function 是至少滿足下列一個條件的函數:
- 接受一個或多個函數作為輸入
- 輸出一個函數
光用講的很抽象,所以我們都來看看例子吧:
function unary(fn) {
return function one(args) {
fn(args)
}
}
function binary(fn) {
return function two(arg1, arg2) {
fn(arg1, arg2)
}
}
function f(...args) {
return args
}
let foo = unary(f)
let boo = binary(f)
foo(1, 2, 3, 4) // [1]
boo(1, 2, 3 ,4) // [1, 2]
unary
和 binary
兩個 function 就是 higher order function,他們接收了 function 當作 input,也把 function 當作 output 來 return。
所以當我們把 f
丟進 unary
的時候,unary
的 return 的 output 會是只接受一個 argument 的 function,所以 line18 的 foo
其實等同於 line2 的 one
這個 function,因此 foo
只會接受一個 argument。
透過了解 unary
和 binary
兩個 function 能夠注意到一件事情,那就是他們可以改變 function 的 shape,比如 unary
會把傳進來的 function 變成只接受一個參數,當我們需要改變 function 接受參數的數量時,就能夠透過它們來進行調整,因此我們可以把它們稱為 adapter,原因就是這類的 adapter function 能夠讓 function 的 shape 改變,進而接收不同數量的 input。
簡而言之,如果樂高積木形狀不同的話,可以透過 adapter 來調整積木的形狀,讓這兩塊積木能夠組裝在一起,如果要使用 FP,需要試著熟悉這樣的模式,不只是使用現成的 function 得到想要的輸出,當 function 的 shape 不合的的時候,懂的使用 HOF 來調整 shape 也是很重要的。
Adapter Examples
接下來再示範幾個 adapter,看看我們能怎麼調整 function 的 shape。
rest parameters
因為從這章節開始 rest parameter 這個語法使用頻率變得非常高,而我在這之前其實對這語法沒有到很熟,大部分都只用到 spread operater 而已,所以稍微筆記 rest parameter 的用法。
來看看這段程式碼:
function foo(a, b, ...args) {
console.log('a:', a)
console.log('b:', b)
console.log('args:', args)
}
foo(1, 2, 3, 4 ,5)
// a: 1
// b: 2
// args: [3, 4, 5]
a 和 b 分別為 1 和 2 很容易理解,值得注意的是 args 的值,rest parameter 的特性在於可以把剩下傳進來的 arguments 收進去一個陣列裡面,因此 args 的值會是 [3, 4, 5]
,因為剩下傳進來的 argument 都放在這個陣列裡面了。
另外 rest parameter 也很常和 spread operator 一起使用,更詳細的內容可以參考 MDN - 其餘參數(rest parameter)
範例 1 - reverseArgs
function reverseArgs(fn) {
return function reversed(...args) { // [1, 2, 3, 4]
fn(...args.reverse()) // [1, 2, 3, 4] -> ...[4, 3, 2, 1] -> 4, 3, 2, 1
}
}
function f(...args) {
return args
}
var g = reverseArgs(f)
g(1, 2, 3, 4) // [4, 3, 2, 1]
reverseArgs
在這裡做的事情就是在 line2 將 paramter 集合起來,變成一個 array;接著再透過 reverse()
把 args 反轉,反轉後的 args 再接著被 spread operator 展開,變成被展開的 arguments。
被展開的 arguments 到了 f
這個 function 後再次透過 rest parameter 語法集合起來變成一個 array,然後被 return,也就得到了 [4, 3, 2, 1]
這個結果了。
透過 reverseArgs
,我們可以把傳進來的參數順序顛倒。
範例 2 - spreadArgs
function spreadArgs(fn) {
return function spread(args) {
fn(...args)
}
}
function f(x, y, z) {
return x + y + z
}
var g = spreadArgs(f)
g([1, 2, 3]) // 6
spreadArgs
用處在於把傳進來的陣列展開,變成一個一個分開的 argument。
在 Function Programming 裡,通常就被稱為 apply
。(詳情可參考 Function.prototype.apply())
範例 3 - unspreadArgs
臨時考來了!如果 spreadArgs
可以把陣列展開,那怎麼樣把分散的 arguments 收集成一個陣列呢?也就是 unapply
,這要怎麼做呢?
function unspreadArgs(fn) {
return function unspread(...args) {
fn(args)
}
}
其實就只是把 spreadArgs
做的事情反過來做而已~
Adapter 的應用場景
可以透過思考以下兩點,來想想自己是否需要創造 adapter:
- Can I change the shape of my function at definition so that it fits better?
- If not, can I make an adapter that changes the shape?
可以的話,FPer 會比較傾向從舊有的 utilities 裡面找尋、堆疊出想要的結果,為什麼呢?創造自己的 utilities 不也挺好玩的嗎?的確,但這同時也會讓 code 變得沒那麼令人熟悉。
Function Programming 有個特點是,假設你熟悉了 Ramda 或其他現成的 Library 後,通常就會了解那些 utilities 的命名慣例和作用了,因為它們的使用方式和命名幾乎都差不多,所以重新創造新 utilites 的缺點就在於破壞了這個熟悉度,FP 喜歡用舊有的樂高積木,當真的沒辦法的時候,才會鑄造自己適用的積木。