--- tags: Functional Programming --- # FP - 組合函數 Compose vs Pipe # 為什麼需要組合函數 接續上篇 [curry](https://hackmd.io/@ChrisW/SJNNgcyWq),我們講解了如何將一個函式拆解成一個個可重複利用的 curry function,[very beautiful,very powerful]((https://www.youtube.com/watch?v=Z-TxcpB6yNY))。 但連結過多的 function 將會讓可看性大幅度下低,就如同下面的例子: 目前有一個會抓取物件名稱的 function ```jsx const getName = (person) => person.name; ``` 另一個會將字型變成大寫: ```jsx const uppercase = (string) => string.toUpperCase(); ``` 如果想要拿取一個資料內的名字,並將他轉為大寫,就會像這樣: ```jsx const data = {name:'chriswang'} uppercase(getName(data)) //CHRISWANG ``` 現在我只想要抓到前5個字元: ```jsx const get5Characters = (string)=>string.substring(0,5) get5Characters(uppercase(getName(data))) //CHRIS ``` 接下來隨著需求擴張,我可能還需要 A、B、C、D... 等功能: ```jsx A(B(C(...(get5Characters(uppercase(getName(data)))))) // .... ``` 這樣一來別說你同事會想貓你一拳,連你自己都會想把這行刪掉。 > curry 其實就只是一個將 function [抽象化](https://hackmd.io/MZvwMweFR-Sv1RMTXRvXkg)的過程,重複可使用的 function 能大幅提高程式的簡潔度,更有降低耦合提高內聚的良好功用。 >以上內容都會在說明 [Functional Programming](https://hackmd.io/NeQCFGazQ3i4FC1UtGrP6w) 時詳細說明,你可以先把他暫時想成另一種相比於 OOP 的另一種 Design Pattern 就好 。 # Compose 開門見山的說,這就是 compose : ```jsx const compose = function(g, f) { return function(x) { return g(f(x)); }; }; // 或者採用 arrow function 的寫法 會更加整潔 const compose = (g,f) => x => g(f(x)); ``` 我們可以藉由 compose 將兩個 function 結合,達到讓程式碼簡化的目的。 可以注意到的是,在 compose 的定義中,f 會在 g 之前執行,而建立一個**由右至左的資料流**,也可以稱為左傾(left direction),注意是由右至左,而非由內而外。 ![其實 compose 這概念也是從數學來的 photo by medium](https://i.imgur.com/TCOs2DZ.png) 現在將上述的例子使用 compose 實作看看: ```jsx= const data = {name:'FrankChou'} // 將要先執行的 function 放在右邊 const uppercaseName = compose(uppercase,getName) uppercaseName(data) // FRANKCHOU ``` 那麼問題來了,要如何串接 2 個以上的 function 呢? ## 合成 n 個 function ### 結合律 前面也提到 compose 這個概念來自於數學,因此他擁有一般計算的特性 — 結合律,意思就是說: ```jsx // 結合律(associativity) // 不管我是先將哪個 fumction 組合,其結果都會是一樣的 const associative = compose(g, compose(f, h)) === compose(compose(g, f), h); ``` 因此我們就能透過 compose 不斷的組合,不過還有更好的選項。 ### 可變量參數 透過展開運算子(...) 和 reduceRight() 重新定義 compose 來達成 2 個以上的任意組合: ```jsx const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x); ``` 接下來我們將上述例子中全部的 function 組合再一起: ```jsx const uppercaseName = compose(get5Characters,uppercase,getName) const data = {name:'FrankChou'} uppercaseName(data) // FRANK ``` 這邊需要在注意一次順序,由於 compose 是由右至左的運算過程,因此會將最需要先執行 `getName` 放在第一個。 # Pipe pipe 和 compose 唯一的差別就是執行順序( `reduce` & `reduceRight`),因此可以說 pipe 就像是反向的 compose: ```jsx const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x); ``` pipe是由左至右的運算方式,所以在使用 pipe 時,會像這樣: ```jsx const uppercaseName = pipe(getName,uppercase,get5Characters) const data = {name:'FrankChou'} uppercaseName(data) // FRANK ``` # Point free > Pointfree style means never having to say your data 意思是指,function 不必提及要操作的資料是什麼樣的。`First Class Function` ( 將 function 當作參數傳送)、 [FP - 柯里化 (Curry)](https://hackmd.io/@ChrisW/SJNNgcyWq) 及 `compose` / `pipe` 協作起來非常有助於建立這種模式。 先來看個沒有 point free 的版本吧: ```jsx const snakeCase = function(word) { return word.toLowerCase().replace(/\s+/ig, '_'); }; ``` 在這個 function 中,一定要先定義一個參數 `word`,才能繼續設定後續的操作,聽起來很正常吧,那現在來看看 point free 的做法: ```jsx const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase); ``` 這兩者的差別就在於,當需要實做 function 時,要不要提及參數本身。 Pointfree 幫助我們移除不必要的命名,讓程式碼保持簡潔和通用 同時 Pointfree 對於 functional 來說也是個很好的試金石,因為這能幫助我們了解一個 function 是否為接受回傳回來的下一個 function,例如 `compose` 無法用於 `while` 。 並不需要特意去追求 Pointfree —— 就跟 PureFunction 一樣,該用的時候使用,不該用的時候回歸原本就好了。 > PureFunction 意指相同的輸入,永遠會得到相同的輸出,而且沒有任何顯著的副作用 # DeBug 先看個範例: ```jsx const R = require('ramda'); const data =['frog', 'eyes'] let reverse = R.reduce((acc, x) => [x].concat(acc), []); let exclaim = (x) => x + '!' let toUpperCase = (x) => x.toUpperCase() let angry = compose(exclaim, toUpperCase); ``` 接著我們來看看以下這個有什麼問題: ```jsx const latin = compose(angry ,reverse); latin(data) ``` 首先我們知道 compose 是由右至左( <- )的運算方式: - `reverse()` 接收 array 並無問題 - 但 `angry()` 內的兩個 function 都只接受 string ⇒ `angry()` 拿到的資料卻是 array ⇒ 出事啦 阿北 於是我們發現問題了,我們將 angry 包裹上 map,讓他接收到 array 內的 string, 問題解決 ```jsx const latin = compose(R.map(angry) ,reverse); latin(data) //[ 'EYES!', 'FROG!' ] ``` 但是,這並不好找誒,所以我們必須想出一個辦法 ## Trace 我們不可能有這麼多的時間,在出錯時一步一步往回推,所以需要一個機制來幫助快速找到問題點 定義一個會產生 log 的 function,以下只有設置提醒的 'tag' 和目前傳進來的資料: ```jsx const trace = curry(function(tag, x) { console.log(tag, x); return x; }); ``` 以上個例子來講,我們將 function 放置在我們覺得沒問題的 function 之後: ```jsx const latin = compose(angry,trace('after reverse data :'),reverse); latin(data) // after reverse data : [ 'eyes', 'frog' ] // error message : ... ``` 可以輕易地發現問題出在哪裡 — 我需要 string 但卻傳送了 array 進來。 > 這個 trace function 可以算是一個簡單的示範,如果 compose 之中有更多複雜的資料流,就可以使用更多的監控方式。 # 總結 事實上 pipe compose 等實用的功能都有人整理成函式庫,例如:[ramda](https://www.npmjs.com/package/ramda)、[lodash](https://www.npmjs.com/package/lodash)、[underscore](https://www.npmjs.com/package/underscore)。 他們的作用都是大神們整理好的實用 function,讓人不用費心理解底層實作就能立即使用。 # 參考連結 知乎 [https://zhuanlan.zhihu.com/p/52207982](https://zhuanlan.zhihu.com/p/52207982) freecodecamp [https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/](https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/) medium [https://betterprogramming.pub/compose-and-pipe-in-javascript-medium-d1e1b2b21f83](https://betterprogramming.pub/compose-and-pipe-in-javascript-medium-d1e1b2b21f83) gitbook [https://jigsawye.gitbooks.io/mostly-adequate-guide/content/ch5.html](https://jigsawye.gitbooks.io/mostly-adequate-guide/content/ch5.html)