google code prettify

2016年12月13日 星期二

[F2E_Functional Programming] ECMAScript 5.1 add new Array Functional Programming API

以下內容來自Will保哥,節錄部分作成筆記,理解函式編程核心概念與如何進行 JavaScript 函式編程
需先了解 JavaScript 函式編程,再進一步了解如何使用API 函式編程的高階函式


JavaScript 函式編程
要透過 JavaScript 程式語言來開發出函式編程的程式碼,意味著你所寫的 JavaScript 程式碼必須符合函式編程的一項或多項重要概念,其中最重要的應該就是「一等公民與高階函式 ( First-class and higher-order functions )」這項了。

在 JavaScript 程式語言中,函式 ( functions ) 包含兩個非常重要的觀念:
  1. 函式為一級物件 ( first-class object )
  2. 函式提供了變數的作用域 ( scope )
在函式程式語言中,通常函式本身就要跟其他物件一樣都視為「一等公民」( First-class ),剛好 JavaScript 就具備這樣的特性。
var add = function (a, b) { return a+b; }
var calc = function (op, a, b) { return op(a, b) };

calc(add, 1, 2); // 3
程式筆記:op 指的是要帶入的函式,add 是被代入函式,所以回傳結果 3



Higher-order function (高階函式)
要達成 Higher-order function 的必要條件,就是符合下列兩項任何一項以上條件,
就可以稱為「高階函式」:
  • 可以將函式物件當成參數傳入另一個函式
  • 可以將函式物件當成另一個函式的回傳值
以下就是一個 JavaScript 函式寫成「高階函式」的範例,這裡的 search 函式回傳的是一個函式物件,這就符合了「高階函式」的基本要求:

var search = function (pattern) {
  return function(str) {
    return str.search(pattern);
  };
}

var searchByWill = search(/Huang/);  //  => str.search(/Huang/)
searchByWill('Will Huang'); // 5   

程式筆記:回傳str.search(/Huang/),因此輸入字串就會去找Huang的位置。

請注意:函式就算定義為「高階函式」,也不一定就能稱為「函式編程」,符合函式編程有一定的要件,你還必須確保該函式要能「避免改變狀態」、「避免可變的資料」以及擁有「純函式」等特性。



ECMAScript 5.1 - 此篇文章會提到這三個API 函式編程的高階函式
Array.prototype.filter() - JavaScript | MDN 
Array.prototype.map() - JavaScript | MDN
Array.prototype.reduce() - JavaScript | 
JavaScript 規格中加入了幾個陣列的 API,這幾個 API 函式算是符合函式編程的高階函式
在開始說明這幾個高階函式前,我先新增一個陣列,當成之後範例程式碼的輸入資料:
var people = [
  {
    "name": {
      "first": "Will",
      "last": "Huang"
    },
    "company": "MINIASP"
  },
  {
    "name": {
      "first": "James",
      "last": "Huang"
    },
    "company": "Coolrare"
  },
  {
    "name": {
      "first": "Jeff",
      "last": "Wu"
    },
    "company": "MINIASP"
  }
]


Array.prototype.filter() - JavaScript | MDN 
假設我們的需求是希望能透過程式找出 people 物件中 last name 為 'Huang' 的人,如果我們用傳統的程式風格來寫,程式碼可能會長這樣:
var i, person, filtered_people = [];
for(i=0; i<people.length; i++) {
  person = people[i];

  if(person.name.last === 'Huang') {
    filtered_people.push(person);
  }
}

console.log(filtered_people);

如果們改用函式編程的寫法,改用 Array.prototype.filter() 來過濾陣列,那麼程式碼會變成這樣:
var lastNameIsHuang = function(person) {
  return person.name.last === 'Huang';
};

var filtered_people = people.filter(lastNameIsHuang);
console.log(filtered_people);
程式筆記:程式變得很好維護,過濾方法整個抽出來



Array.prototype.map() - JavaScript | MDN
這個陣列的 map() 函式也是一個高階函式,他與 filter() 不同的地方在於:
  • filter() 函式會過濾原本陣列中的資料,並回傳一個全新的陣列。
  • map() 函式則會轉換原本陣列中的每一個元素,並回傳一個全新的陣列。
var new_people = people.map(function(person) {
  return {
    name: person.name.first + ' ' + person.name.last,
    company: person.company
  };

});

console.log(new_people);


程式筆記:用起來跟C# 的Automapper很類似,可以依造需求改變排列

你可以發現 map() 函式會讀入每一個陣列元素,並且依序傳入 map() 的回呼函式中,每一次的回呼函式執行都只要回傳「新元素」即可,最後 map() 回傳的結果將會是一個全新的陣列,而且陣列中的每個元素也將會是全新的物件。

這邊的 map() 函式,我直接照著字面翻譯,就是一種「對應」功能,把一份完整的陣列「對應」到另一份全新的陣列,並且回傳這份全新、對應過的陣列。



Array.prototype.reduce() - JavaScript | MDN
假設我們的需求是希望能透過程式計算出每個元素的 company 屬性的總字元數,傳統的寫法一定是先宣告一個變數,然後跑個迴圈計算每個元素中的數值,不過函數編程的寫法就可以靠 reduce() 函式來幫我們完成這個連續計算作業。
var total_company_chars = people.reduce(function(sum, person) {
  return sum + person.company.length;
}, 0);

console.log(total_company_chars);

1.reduce() 函式會將 people 陣列中每個元素依序傳入回呼函式中執行
2.第一次執行回呼函數
第一個參數 sum 會傳入 reduce() 函式傳入的第 2 個參數 0
第二個參數 person 則會傳入陣列中的第 1 個元素
        回呼函式的回傳值,預設會是第 2 次執行回呼函式的第一個參數
3.第二次執行回呼函數
第一個參數 sum 會傳入上一次執行回呼函數的回傳值
第二個參數 person 則會傳入陣列中的第 2 個元素
回呼函式的回傳值,預設會是第 3 次執行回呼函式的第一個參數
4.依此類推 … 直到傳入陣列中的最後一個元素

程式筆記:此函式用法建議先去看MDN,將公司名稱字元加總
  • map() 函式則會將原本陣列中的每一個元素「對應」成另一個全新的陣列。
  • reduce() 函式則會將陣列中的元素中每個元素「縮減」成一個結果,你也可以把 reduce() 想像成「彙整所有陣列元素,透過回呼函式的連續計算獲得一個縮減後的結果」。

剛剛講的這三個高階函式 filter()、map() 與 reduce() 是可以搭配使用的,如果正確使用這 3 個高階函數,並使用函式編程的方式來撰寫,我們的程式碼就會非常易讀、易懂、方便測試。



修改一下需求:
我想過濾出 last name 為 Huang 的陣列元素 Array.prototype.filter() - JavaScript | MDN 
將篩選過後的陣列對應出全新的物件格式Array.prototype.map() - JavaScript | MDN
計算所有新陣列中每一個元素 name 屬性累計的字元數Array.prototype.reduce() - JavaScript | MDN

people
    .filter(function(person) {
      return person.name.last === 'Huang';
    })

    .map(function(person) {
      return {
        name: person.name.first + ' ' + person.name.last,
        company: person.company
      };
    })

    .reduce(function(sum, person) {
        return sum + person.company.length;
    }, 0);

你從上述程式可以看出,這段程式確實符合函式編程的幾個重要特性:

  • 避免改變狀態
  • 避免可變的資料
  • 純函式
  • 延遲評估 (Lazy evaluation)

函式語言並非萬能,別忘了函式編程 ( functional programming ) 只是一種程式設計方法,他用不同的思考方式來解決問題,在某些情境下,使用函式編程確實能帶來極大效益,但不代表他很適合用來解決所有問題。有的時候使用函式編程反而會犧牲許多程式的執行效率,這是拿非函式變成語言來寫函式編程的常見問題,用 JavaScript 來寫函數編程也會有相同的問題存在,而且通常改用函數編程後,執行效能會比跑一般迴圈慢個 3 ~ 5 倍之多 (比較程序編程與函數編程的效能差異),當你處理資料過大時,就比較會有機會遇到效能問題。不過撰寫網頁應用程式時,似乎不太容易發現有這麼大的效能差異,因為我們本來就不會在網頁中處理大量的資料來源,而且現今的電腦與瀏覽器在執行 JavaScript 的時候,真的還蠻快的!

最後,推薦一個由 ReactiveX 設計的 Functional Programming 教學網頁 ( Functional Programming in Javascript ),這個頁面總共有 41 個 Functional Programming 練習題,你要一關一關過才行,建議不要跳關,做到最後,我保證你一定可以完全理解如何在 JavaScript 使用 Functional Programming 開發程式!

相關連結

沒有留言:

張貼留言