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)

xy 就是 parameter,34 就是 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]

unarybinary 兩個 function 就是 higher order function,他們接收了 function 當作 input,也把 function 當作 output 來 return。

所以當我們把 f 丟進 unary 的時候,unary 的 return 的 output 會是只接受一個 argument 的 function,所以 line18 的 foo 其實等同於 line2 的 one 這個 function,因此 foo 只會接受一個 argument。

透過了解 unarybinary 兩個 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:

  1. Can I change the shape of my function at definition so that it fits better?
  2. If not, can I make an adapter that changes the shape?

可以的話,FPer 會比較傾向從舊有的 utilities 裡面找尋、堆疊出想要的結果,為什麼呢?創造自己的 utilities 不也挺好玩的嗎?的確,但這同時也會讓 code 變得沒那麼令人熟悉。

Function Programming 有個特點是,假設你熟悉了 Ramda 或其他現成的 Library 後,通常就會了解那些 utilities 的命名慣例和作用了,因為它們的使用方式和命名幾乎都差不多,所以重新創造新 utilites 的缺點就在於破壞了這個熟悉度,FP 喜歡用舊有的樂高積木,當真的沒辦法的時候,才會鑄造自己適用的積木。

References