iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 12
1
Software Development

Functional Programming in JS系列 第 12

實作經典 HOF 之 filter、map、reduce

備註:關於昨天的 Higher-order function 覺得寫的不夠白話,所以回去大改了一下,然後把實作 filter 拉到此篇。

https://ithelp.ithome.com.tw/upload/images/20200912/20106426OBA6yiFaPk.jpg
自己面試時被考過好幾次實作原生 Higher-order function,覺得蠻值得拿出來寫一篇文。

Filter

讓我們來想想若今天要實作 filter 但又不能用原生的 Array method filter,那該怎麼寫

// to do list
var taskList = [
  {
    id: 0,
    title: 'Buy Avocado',
    status: 'pending',
    dueDate: '2020-05-31',
  },{
    id: 1,
    title: 'Clean house',
    status: 'complete',
    dueDate: '2020-05-21',
  },{
    id: 2,
    title: 'Implement js Array filter',
    status: 'pending',
    dueDate: '2019-05-21',
  }
];

首先想要 filter 出 status: 'pending' 的 list ,用很直覺的 OOP 寫法可能會

function Filter(taskArray){
  // 準備 filterTask 這個容器存結果
  let filterTask = []

  for(let i=0; i<taskArray.length; i++ ){
   // 把 status 為 'pending' 的存進 filterTask 裡
    if(taskArray[i].status === 'pending'){     
      filterTask.push(taskArray[i])
    }
  }
  return filterTask;
}
const filterPending = Filter(taskList);
console.log('filterPending: ', filterPending)

/*
[
  {
    dueDate: "2020-05-31"
    id: 0
    status: "pending"
    title: "Buy Avocado"
  },
  {
    dueDate: "2019-05-21"
    id: 2
    status: "pending"
    title: "Implement js Array filter"
  }
]
*/

若今天除了列出 status = 'pending' 還要特定日期 dueDate = '2019-05-21' 的 task 可能就會變

function Filter(taskArray, {status = 'pending', dueDate = '2019-05-21'){
  let filterTask = []

  for(let i=0; i<taskArray.length; i++ ){
   // 把 status 為 'pending' 的存進 filterResult 裡
    if(taskArray[i].status === status && taskArray[i].dueDate === dueDate){     
      filterTask.push(taskArray[i])
    }
  }
  return filterTask;
}
const filterResult = Filter(taskList, {
  status: 'pending', 
  dueDate:'2019-05-21' 
});
console.log('filterPending: ', filterResult)
/* 
{ 
  id: 2, 
  title: "Implement js Array filter", 
  status: "pending", 
  dueDate: "2019-05-21"
}

日後若又有新的 filter 想要列出特定 id,呃 越來越複雜,而且也無法一目瞭然這幾行 code 再幹嘛

function Filter(taskArray, {status = 'pending', dueDate = '2019-05-21', id = null} ){
  let filterTask = []

  for(let i=0; i<taskArray.length; i++ ){
    
    if(id === null && 
      (taskArray[i].status === status && taskArray[i].dueDate === dueDate) ||
      id !== null && taskArray[i].id  ===  id
    ){     
      filterTask.push(taskArray[i])
    }
  }
  return filterTask;
}
const filterResult = Filter(taskList, {
  id: 1
});

console.log('filterPending: ', filterResult)
/* 
{
  id: 0, 
  title: "Buy Avocado", 
  status: "pending", 
  dueDate: "2020-05-31"
}

寫到這邊就會發現,用原本直覺的思維寫出來的程式碼是非常沒有彈性而且難以維護的,每當增加一個需求就必須改寫一次 Filter ,到最後可能會放棄使用這個函式因為看不懂然後另寫一個同樣難維護的 function

如何用 Higher-order functions 改寫

FP 原則就是把 Function 作抽象化並極小化,

抽象化: 不是只有此功能可以用,他是可以運用在所有地方,

這種 Function 大部分都只做一件事情,並且是可以再次利用的,Function 裡面並不會有太多邏輯,因為邏輯是需求定義的,當需求不明就交給外部使用者決定。最後記得符合 Pure Function 的原則則。

以上面例子來說,我們想做的事情就是 “過濾”,而要過濾什麼就交給使用者決定就好 (這邊的 MD 無法在程式碼中有刪節線只好用圖了)

https://ithelp.ithome.com.tw/upload/images/20200911/20106426RKJXQy62Ux.png

在 FP 的世界裡,要把部分邏輯交由外部的使用者決定是很簡單的,我們只要要求使用者傳一個 function 進來,並預期這個 function 會回傳某種值就可以了,以這裡來說就是 Boolean 值。

function Filter(array, fn){  // 改為 array 因為可以過濾所有 array 不只限定在此需求上
  let filterTask = []

  for(let i=0; i < array.length; i++ ){ 
    if( fn( array[i] ) ) {     
      filterTask.push(array[i])
    }
  }
  return filterTask;
}

所以若想要 有兩個 filter list

  • 列出 status = 'pending' 且是特定日期 dueDate = '2019-05-21' 的 task
  • 列出 id: 1 清單
const filter1 = Filter(taskList, list => 
  list.status === 'pending' && list.dueDate === '2019-05-21'
) 

const filter2 = Filter(taskList, (list) => list.id === 1)

是不是簡單超多 !!!

Map

相信來看我文章的人應該都已知道 JS mapreduce 的使用方法這邊就不贅述,但還是想分享當初自己剛學時讓我瞬間理解 mapreduce 的下圖
https://ithelp.ithome.com.tw/upload/images/20200912/20106426LysA7tA7dl.jpg

let ingredient = ['吐司', '番茄', '雞蛋', '生菜']; 

let slice = ingredient.map( x => 切片 x );
// 我知道蛋不是切片不過就將就一下
// => ['切片吐司', '切片番茄', '一片蛋', '切片生菜']

let sandwich = ingredient.reduce((acc, cur) => acc +  cur);

廢話不多說,來實作 map 吧

// Build-in 
[1, 2, 3].map((x) => x + 1); // => [2, 3, 4]
function Map(array, callback){ 
  let task = []

  for(let i=0; i < array.length; i++ ) {   
      task[i] = callback(array[i])
  }
  return task;
}

const count = Map([1, 2, 3], (x) => x + 1)

console.log(count) // => [2, 3, 4]

Reduce

Reduce 是我心目中最神奇的 HOF 了,因為他不是回傳一整個 Array,而可以只回傳一個值

// Build-in 
[1, 2, 3].reduce((acc, cur) => acc + cur); // => 6
function Reduce(array, combine, start){ 
  let current = start;
  for (let element of array) {
    current = combine(current, element);
  }
  return current;
}

console.log(reduce([1, 2, 3], (a, b) => a + b, 0));
// => 6

https://ithelp.ithome.com.tw/upload/images/20200912/20106426xyIwezKc97.png
圖出自 HOF and HOC (in React)

練習

一切從 Array 說起 中提到的範例,也就是 Advent of Code 2019 Day 1 的題目當練習

題目是有一組數字如下

const data = [
  129561,
  125433,
  97919,
  // ...
];

要對每一個數字 除以 3 並無條件捨去至個位數然後再減 2,最後把每個數字做加總。如果用 imperative code 寫大概會像這樣

function calFuel(data) {
  let result = 0;

  for (let i = 0; i < data.length; i++) {
    const ans = Math.floor(data[i] / 3) - 2;
    result = result + ans;
  }

  return result;
}

但用 FP 作法就可以先拆解需求

  • 除以 3
  • 無條件捨去至個位數
  • 減 2
  • 對每個數字加總
const result = data
  .map(x => x / 3) // 除以 3
  .map(Math.floor) // 無條件捨去
  .map(x => x - 2) // 減 2
  .reduce((x, y) => x + y); // 加總

會發現可讀性超高且簡潔,或也可以把 map 合併起來寫成

const result = data
  .map(x => Math.floor(x / 3) - 2) // **除以 3 並無條件捨去至個位數然後再減 2**
  .reduce((x, y) => x + y); // 加總

小結

能夠把函式當成參數傳入另一個函式中其實是非常實用的,除了 JS 的 Array 本身就有提供許多有用的 higher-order methods 外,也可以運用這種特性寫出客製化的 HOF 提供日後重覆使用。
記得 HOF 也就是抽象函式的一種,內容不包含執行步驟的細節,而是在 high level 的角度描述解決方法 (例如上一篇提到的 燉煮、滾刀切塊、切絲)。


參考文章

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

上一篇
Higher-order function
下一篇
[補充] HOF 的好友 HOC (Higher-Order Component)
系列文
Functional Programming in JS30

1 則留言

0
Darwin Watterson
iT邦研究生 4 級 ‧ 2020-09-12 15:17:59

https://ithelp.ithome.com.tw/upload/images/20200912/2010910729hBizTpwi.png

看更多先前的回應...收起先前的回應...
hannahpun iT邦新手 5 級 ‧ 2020-09-12 22:38:48 檢舉

誒,這個圖超讚的耶
之後是否可借我補充進內容?

不是我畫的啦!/images/emoticon/emoticon25.gif
我也是5年前在網路上看到的...原作者應該不可考了吧/images/emoticon/emoticon16.gif

Mandy iT邦新手 5 級 ‧ 2020-09-16 01:04:40 檢舉

看到 reduce 時 噴笑出來 哈哈哈

hannahpun iT邦新手 5 級 ‧ 2020-09-16 13:05:50 檢舉

很好懂的一張圖

我要留言

立即登入留言