FP 精華就是抽象化並拆分成最小單位,一開始也許會覺得多餘,但 FP 中每一個 function 都是可以再利用的,在中大型專案尤其好用 (畢竟你永遠無法知道未來需求會變多複雜,到時候還要花時間 refactor),若以輸入一頭牛輸出漢堡排的過程來說
從頭倒尾只會有一個水管(一個 function),裡面做所有需要做的事情
let 製造漢堡肉 = (input = 牛) => {
把一隻全牛放到肉片機器,然後等他結束再放入絞肉機器打一打,
然後放一堆加工產品打一打捏一捏在放入加工機器,消毒之後放入煮熟機器
最後就是漢堡排啦
}
Function composition is an act or mechanism to combine simple functions to build more complicated ones
會拆分成許多小 function ,每一個 function 就像一根根小水管,小水管之間互相獨立,也遵守 one input, one outtput 概念 (Pure),最後再接再一起。
const 肉片機器 = input => 生牛肉片
const 絞肉機器 = input => 牛絞肉
const 加工機器 = input => 加工程漢堡肉
const 煮熟機器 = input => 煮熟
const compose = (a, b, c) => x => a(b(c(x)))
let 製造漢堡肉 = compose(煮熟機器, 加工機器, 絞肉肉機器, 肉片機器)
// 先執行 肉片機器 然後 絞肉肉機器 然後以此類推
製造漢堡肉(牛); // 輸出漢堡肉
直接來看真實生活中程式碼好了! 請寫出以下 transform1
、transform2
兩個函式
Input: str = 'hello world'
Output: 'HELLO WORLD!'
/**
* @param {String} str
* @return {String}
*/
const transform1 = function(str) {
// 請把 code 寫在這
};
Input: str = 'hello world'
Output: 'hello world!'
/**
* @param {String} str
* @return {String}
*/
const transform2 = function(str) {
// 請把 code 寫在這
};
const transform1 = (str) => {
return `${str.toUpperCase()}!`;
}
const transform2 = (str) => {
return `${str}!`;
}
console.log(transform1('hello world')) // HELLO WORLD!
console.log(transform2('hello world')) // hello world!
看起來沒太大問題,但若需求逐漸變複雜,transform 這個涵式就會越來越大包,並且因為需求全部寫在一起,所以 transform1 跟 transform2 無法共用。例如我想要在 return 前先確定 input 是否為字串,只好像以下這樣加
const transform1 = (str) => {
if(typeof str === 'string'){
return `${str.toUpperCase()}!`;
}
return 'Not a string'
}
const transform2 = (str) => {
if(typeof str === 'string'){
return `${str}!`;
}
return 'Not a string'
}
以上兩個 transform 明明重覆地方就很多,寫成這樣既不好維護也不彈性。
用 Compose 概念就是要切許多獨立的小 function 出來,以上面例子我會拆分成幾個小功能
const toUpper = str => str.toUpperCase()
const exclaim = str => str + ' !'
const isString = str => typeof str === 'string' ? str : 'Not a string'
const toUpper = str => str.toUpperCase()
const toLower = str => str.toLowerCase()
const exclaim = str => str + '!'
const isString = str => typeof str === 'string' ? str : 'Not a string'
const compose = (a, b, c) => x => a(b(c(x)))
let transform1 = compose(isString, toUpper, exclaim) // 先執行 exclaim 然後 toUpper
transform1('hello world') // "HELLO WORLD !"
let transform2 = compose(isString, toLower, exclaim);
transform2('hello world') // "hello world !"
Compose 跟 Pipe 的概念一樣,唯一不同是順序,從下圖可以看到 Compose 會從右到左參數執行而 Pipe 會從左到右執行
compose(
turnRed,
turnBlue,
turnYellow
)(x); // 最後會是紅色
pipe(
turnRed,
turnBlue,
turnYellow
)(x); // 最後會是黃色
個人更喜歡用 pipe,因為使用上更為直覺。以上是寫死三個參數,但實務上你不會知道會有幾個參數需要傳入,搭配 es6 寫的 pipe 可以更為彈性,要傳幾個參數都可以
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
// 傳 3 個參數
let transform1 = pipe(
isString,
toUpper,
exclaim
)('hello world');
transform1; // "HELLO WORLD !"
// 傳兩個參數
let transform2 = pipe(
isString,
exclaim
)('hello world');
transform2; // "hello world !"
當然你可以跟 Curry 一起搭配,寫出更彈性的 code
let add = curry((x, y) => y + x);
let transform1 = pipe(
isString,
add('!'),
toUpper,
)('hello world');
transform1; // "HELLO WORLD!"
// 傳兩個參數
let transform2 = pipe(
isString,
add('?')
)('hello world');
transform2; // "hello world?"
如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您
歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。
想問一下,compose(isString, toUpper, exclaim) 的 isString 是不是應該要擺在最後一個,才會在參數傳進去一開始時判斷?
這麼久才回你XD 對沒錯,他會先執行最後一個函式