iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 16
0
Software Development

Functional Programming in JS系列 第 16

Composition

FP 精華就是抽象化並拆分成最小單位,一開始也許會覺得多餘,但 FP 中每一個 function 都是可以再利用的,在中大型專案尤其好用 (畢竟你永遠無法知道未來需求會變多複雜,到時候還要花時間 refactor),若以輸入一頭牛輸出漢堡排的過程來說

一般思維

從頭倒尾只會有一個水管(一個 function),裡面做所有需要做的事情

https://ithelp.ithome.com.tw/upload/images/20200916/20106426pTDjKfwaAX.jpg

let 製造漢堡肉 = (input = 牛) => {
	把一隻全牛放到肉片機器,然後等他結束再放入絞肉機器打一打,
    然後放一堆加工產品打一打捏一捏在放入加工機器,消毒之後放入煮熟機器
    最後就是漢堡排啦
}

Composition 思維

Function composition is an act or mechanism to combine simple functions to build more complicated ones

會拆分成許多小 function ,每一個 function 就像一根根小水管,小水管之間互相獨立,也遵守 one input, one outtput 概念 (Pure),最後再接再一起。

https://ithelp.ithome.com.tw/upload/images/20200916/20106426oFzoGz1l2B.jpg

const 肉片機器 = input => 生牛肉片
const 絞肉機器 = input => 牛絞肉
const 加工機器 = input => 加工程漢堡肉
const 煮熟機器 = input => 煮熟

const compose = (a, b, c) => x => a(b(c(x)))
let 製造漢堡肉 = compose(煮熟機器, 加工機器, 絞肉肉機器, 肉片機器) 
// 先執行 肉片機器 然後 絞肉肉機器 然後以此類推
製造漢堡肉(牛); // 輸出漢堡肉

小試牛刀

直接來看真實生活中程式碼好了! 請寫出以下 transform1transform2 兩個函式

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 明明重覆地方就很多,寫成這樣既不好維護也不彈性。

Compositione 思維

用 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 vs. Pipe

Compose 跟 Pipe 的概念一樣,唯一不同是順序,從下圖可以看到 Compose 會從右到左參數執行而 Pipe 會從左到右執行

https://ithelp.ithome.com.tw/upload/images/20200916/20106426Wo9DP39ZoB.jpg

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 !"

Pipe + Curry

當然你可以跟 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?"

如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您

歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。


上一篇
[練習] Currying Exercise
下一篇
[補充] Ramda.js
系列文
Functional Programming in JS30

尚未有邦友留言

立即登入留言