這個只是封面,內文才是 Functional Programming 和 Javascript 的相關概念教學

Functional Programming 之 Javascript 簡單概念與教學 ( 以 Ramda 為例 ) / reduce / map / filter / pipe …

今天介紹一下 FP 概念中的 reduce / map / filter / find / pipe… 在 Javascript 中使用範例

Seachaos
tree.rocks
Published in
12 min readDec 7, 2020

--

前言

Functional Programming 是一個很重要的概念,這個概念不分任何程式語言,如果了解的話對於演算法的幫助很大,就像是會開車的人,什麼車的都可以開,換到新車只要了解一下各種按鈕是幹嘛的(?)

這裡我們先淺談一下概念,然後進入範例操作 ( 只要有電腦版網頁瀏覽器 [ Chrome / Safari 佳 ]免安裝 node / npm 之類就可以練習 )

另外本人才疏學淺,本文有誤的地方請多包涵指教

Functional Programming 概念

簡單說明其中一個 functional 概念就是 :丟進去的東西和出來會永遠一樣,不被外部環境干擾,這也讓程式語言開發中的測試化變得穩定與可能,就類似一種陳述事實的概念,最簡單例如:

  • 公里換英里: function km2mi(km) { return km * 0.621371 }
  • 圓面積: function area(diameter) { return diameter * diameter * 3.1415926; }
說好不講例外現象…

這個定義我們先撇開神秘的微觀物理、哲學、魔法、超自然力量等等例外,我們講的就一般人 ( 不當人類的例外 ) 的認知, 1 公里等於約 0.621371 英里,這個不會因為你在高山、海裡、月球、火星而改變;圓面積公式也是一樣的道理,半徑 x 半徑 x pi 這種亦不會因為你的地點與環境而改變。

有了以上概念我們套到程式語言中,這種演算法不會因為你今天程式執行幾次、變更語言、放到前端、後端、資料庫改變、系統上線變數改了之類產生變化,所以可以穩定的驗證功能無誤。

其實 Python 在這方面本身就做得很好了;但今天我們以 Javascript 與 Ramda 作為示範

另外本人偏好使用 Ramda , Javascript 這方面其實套件不少,各家大同小異,例如常見的 Lodash ( https://github.com/lodash/lodash ) 也是不錯用

範例資料

const data = [
{name: 'A', at: 'moon', fuel: 10 }, {name: 'G', at: 'venus', fuel: 15 },
{name: 'B', at: 'earth', fuel: 12 }, {name: 'F', at: 'moon', fuel: 19 },
{name: 'C', at: 'mars', fuel: 14 }, {name: 'H', at: 'venus', fuel: 22 },
{name: 'D', at: 'venus', fuel: 16 }, {name: 'I', at: 'earth', fuel: 16 },
{name: 'E', at: 'pluto', fuel: 16 }, {name: 'J', at: 'moon', fuel: 21 },
];

以上是我們這篇文章所會用到的測試資料,假設有不同的太空船在不同的星球附近

基本測試

如果你沒有開發環境,或是懶得設置,只是想玩看看,可以開啟瀏覽器到 Ramda 官方的 Document 頁面( https://ramdajs.com/docs/

使用 Chrome 方法

google chrome 的 console 執行 javascript

按下 Cmd +option (alt) + i 就可以開啟 ( 忘了是不是要啟動開發者模式 ? )
然後選擇 console ,先打 R 然後 Enter 確定有載入 Ramda 成功,然後貼上測試資料,就可以開始試驗

使用 Safair 方法

safari 開啟開發者模式

Mac 的 Safari 開啟開發者模式 :
偏好設定 裡面的 進階,打勾 開發者模式(中文版應該類似是這樣 … 很久沒用了 … )

然後按下 Option ( Alt ) + CMD + i 進入 console

safari 的 console 執行 javascript

如圖
先打 R 然後 Enter 確定有載入 Ramda 成功,然後貼上測試資料,就可以開始試驗

Node.JS 安裝

如果你已經熟悉 Node.js 的 npm / yarn 就可以使用 `yarn add ramda` 來進行安裝,然後看你是 React / React-Native 可能需要

import * as R from 'ramda'

import R from 'ramda'

Function Programming 之 filter / find 介紹

現在我們開始來看應用問題,就是如果我要找出所有在金星的飛船該怎麼做? 有人覺得可以先做一個空陣列,然後迴圈跑過所有資料,找到在金星的飛船放入陣列,例如 :

var fleet = [];
for (const ship of data) {
if (ship.at == 'venus') {
fleet.push(ship);
}
}
console.log(fleet);

以上是傳統寫法,然後問題是 fleet 這個變數容易被惡搞,或是散亂在程式碼中的任何地方(如果多個地方都要使用)

我們來看看 functional 的方法

function findShipAtVenus(ship) {
return ship.at == 'venus';
}
var fleet = R.filter(findShipAtVenus, data);
console.log(fleet)

如此簡單與直白,我們只要使用 filter 與 function 來定義條件
我們這邊的條件 function 叫做 findShipAtVenus
findShipAtVenus 沒有其他雜質(變數)會來干擾他的運作

filter 概念就是會將陣列中的資料跑過一輪,然後只要條件 function return true 的就會保留下來 ( 所以不想要的資料可以 return false 來過濾 )

find 和 filter 是差不多的用法,只是 find 只會回傳一個結果,如下圖比較

filter 與 find 的不同

所以我們的 function 可以重複利用,並且經過測試的話可以確保他的穩定與正確

Function Programming 之 map介紹

map 其實也是和 filter 類似概念,只是他回傳的是處理過的資料

我們來假設一個問題,每個飛船的燃料最多容量是 30 單位,然後我們想要將所有飛船的燃料轉換成百分比,這邊怎麼做?

當然傳統做法可以 ( 例如: 迴圈,產生新資料 / 更新 … <- 更新資料可能引發地雷 )
但是我們可以透過以下範例

function mapFuelToPercentage(ship) {
return {
...ship,
fuel_percentage: parseInt((ship.fuel / 30) * 100) + '%'
}
}
var new_data = R.map(mapFuelToPercentage, data);
console.log(new_data);

得到 new_data,然後本來的 data 並沒有被改動,這也是 functional 的精神之一,不會去改變外部輸入的資料,確保結果一直都是一致 (就像是正常的考試中你不會去改題目 … )

其中 new_data 每個飛船都多了 fuel_percentage 屬性,可以透過下圖方法 log 出比較整齊的資料 ( 一樣使用 map + function 來看 )

function displayStatus(ship) {console.log(`${ship.name} at ${ship.at}. fuel: ${ship.fuel_percentage}`);}R.map(displayStatus, new_data);

Function Programming 之 reduce介紹

其實這部分有點像是 map + 遞迴的概念,他會將所有的 function 跑過一遍 (即 map ),然後結果會整合 ( 即 reduce )

我們直接看應用問題:
如果我想找出所有在月球的飛船燃料總和怎麼做?

分解一下問題,解法順序可能是 :
1. 找出月球的飛船 ( 用 filter )
2. map 出燃料
3. 加總 ( sum ) 出數值

大概會是如下

function findShipAtMoon(ship) {
return ship.at == 'moon';
}
function mapFuel(ship) { return ship.fuel; }
function sum(ar) { let v = 0; for(const i of ar) { v += i }; return v};
var fleet = R.filter(findShipAtMoon, data);
var fleet_fuel = R.map(mapFuel, fleet);
var total_fuel = sum(fleet_fuel);
console.log('moon fuel:', total_fuel);

請先不要管上面的 sum function, 那個只是數值加總用

插播一下 pipeline 概念

在我們講解 reduce 以前,上面的做法其實還有 functional programming 中的 pipeline 概念可以使用,讓你寫更少的程式碼

因為 function 不受外部變數干擾的概念,所以可以達成類似組合技的概念
觀察一下上面的流程是 filter -> map -> sum,所以可以組合起來變成

function findShipAtMoon(ship) {
return ship.at == 'moon';
}
function mapFuel(ship) { return ship.fuel; }
function sum(ar) { let v = 0; for(const i of ar) { v += i }; return v};
var total_fuel = R.pipe(
R.filter(findShipAtMoon),
R.map(mapFuel),
sum
)(data);

console.log('moon fuel:', total_fuel);

上面粗體的 R.pipe 就是一個組合技的概念

我們回頭講 Reduce

上面的步驟要透過好幾次的回圈完成,對於演算法複雜度來說自然有點不好,這時候我們可以使用 Reduce,這個概念可以用來解決很多複雜的問題

我們再看一下之前解題步驟 :
1. 找出月球的飛船 ( 用 filter )
2. map 出燃料
3. 加總 ( sum ) 出數值

有了 Reduce 可以直接變成 ( map 概念 ):
1. 這船是不是在月球 ?
2. 不是的話不處理,是個話就加總燃料

程式碼範例如下 :

function reduceMoonFuel(total_fuel, ship) {
if (ship.at != 'moon') {
return total_fuel;
}
return total_fuel + ship.fuel;
}
var total_fuel = R.reduce(reduceMoonFuel, 0, data);
console.log('moon fuel:', total_fuel);

程式碼乾淨很多,但是我想很多人應該還是一頭霧水,所以我們看以下圖片

reduce 初始值

R.reduce 參數為 function , 初始值, 資料 (注意:有些程式語言或是套件的 reduce 不一定有初始值這個參數 )

reducer 概念就是將 function return 的結果累加,我們用中文來解釋,例如 :

計算燃料 () { 在月球就回報燃料 / 不在月球就回報 0 )
計算燃料(shipA) + 計算燃料(shipB) + 計算燃料(shipC) + ...

如果我們改成初始值改成 100,燃料就會是 150 ( 100 + 50 ),所以可以發現這邊是不停的累加概念

map 與 reduce 概念

關於 sum

所以我們上面 pipe 中有用到一個 sum function, 套用 reduce 的概念很簡單,即是

function sum(ar) { let v = 0; for(const i of ar) { v += i }; return v};
改寫成
function sum(a, b) { return a + b; }

再回頭看一次上面 pipeline 範例就可以改成

function findShipAtMoon(ship) { return ship.at == 'moon'; }
function mapFuel(ship) { return ship.fuel; }
function sum(a, b) { return a + b; }
var total_fuel = R.pipe(
R.filter(findShipAtMoon),
R.map(mapFuel),
R.reduce(sum, 0), // 其實 R.sum 就已經內建了
)
(data);
console.log('moon fuel:', total_fuel);

當然我們已經瞭解了 reduce,自然也可以用 reduce 來解決比較快

結語

以上是超基本的 functional programming 範例
其實 Javascript 的 Array 也已經內建很多 function 了,不過本人還是習慣透過 Ramda 操作,因為感覺可讀性與部署彈性比較高 ( 難保有些 JS 環境不一定支援 )

另外 Ramda 官方還有不少有趣的 function 大家可以自行看看 https://github.com/ramda/ramda/wiki

希望可以幫大家在計算資料上可以少寫一些 Code

--

--