iT邦幫忙

2021 iThome 鐵人賽

DAY 8
1
Modern Web

Javascript 從寫對到寫好系列 第 8

Day 8 - Functional Programming 初探 (1) - HoF 與 Side Effects

前言

Functional Programming 其實是我相對不熟的主題,但因為在寫一些較難的程式時,往往會突然感受到有這些神奇的力量存在(?),再加上現在 forEachmapfilter 這麼普及,總覺得趁這個機會好好整理一下。

Functional Programming 思維

Functional Programming (簡稱 FP),是一種撰寫風格,我覺得更像是一種抽象化的思維,因為用這種思維下去寫的 code,會以 function 為操作的主體。

我覺得 FP 有點像是生產線的思維,第一個員工專門負責備料,第二個員工會接著用這些材料製作成料理,第三個員工就把這些料理送到客人手上。

而每一個員工都像是一個 function,它們不管其它的,只管自己手上的任務,它們的台詞可能是像這樣:

  • 「我就只會切菜,我就專門切菜,切菜以外的我都不管」
  • 「只要上一個員工有給我菜,我一定會把它切好傳給下一個員工」
  • 「我不會去影響其他員工,連聊天都不會」

讓大家對 FP 有個超級基礎的概念,但今天還不會真的寫到 code,因為我們要先來了解很多 FP 的名詞跟觀念:

First-class(一等公民)

要寫 FP 的首要條件就是,function 必須是這個語言的一等公民,代表跟其他資料型別具有同等地位,也就是要擁有這些特性:

  • function 可以用來賦值給變數
const addNum = (x, y) => x + y; 
  • function 能被當作參數傳入
arr.map(num => num + 1);
  • function 能被當作回傳值
const addNum = (x) => {
  return (y) => {
    return x + y;
  };
};

這是硬條件哦!少了這個就不能叫 FP 了。

Higher-order Functions(高階函式)

以下兩者符合一項,即是高階函式(簡稱 HoF):

  • 可以將函式當成參數傳入另一個函式
  • 可以將函式當成另一個函式的回傳值

看到這兩句的當下會愣住,想一下會覺得:「蛤?函式裡面還有另一個函式?我函式你的函式!聽起來好強好高階哦!」 - 高階函式

對不起,這個詞真的不是這樣來的。

雖然我們一般寫 function,會拿來當參數的,大部分都是 string、array 或 object 之類的。

但仔細一想卻會發現:

arr.forEach(() => {});
arr.map(() => {});
arr.filter(() => {});
arr.reduce(() => {}, initialValue);

啊啊啊原來到處都在拿 function 當參數啊!雖然也沒有很普遍,但起碼不算陌生。不過。。。

HoF 帶來了什麼好處?

比起 HoF 「是什麼」,網路上反而很少在討論「為什麼」要有 HoF?它給 FP 帶來了什麼好處?

其實我還真不知道,於是我也乖乖去 google 了一下:

functional programming higher order functions "why"

沒錯我還特地把 why 用雙引號框起來,才比較找得到

得到一個最重要的結論是,HoF 讓程式可以比較容易「抽象化」。

我試著講講我的理解,這邊很期待能有朋友一起補充。

比較 HoF 的 before & after

比如我們熟悉的 filter 就是 HoF,那如果在沒有 HoF、沒有 filter 的情況下,我們要怎麼做到「篩選」這件事呢:

// 篩選出 10 以下的數字
const arr = [3, 6, 9, 12, 15];
const lessThanTen = [];
for (let i=0; i<arr.length; i++) {
    if (arr[i] < 10) {
        lessThanTen.push(arr[i]);
    }
}
console.log(lessThanTen);

執行結果

[3, 6, 9]

那現在使用 HoF 來做,會變成:

// 篩選出 10 以下的數字
const arr = [3, 6, 9, 12, 15];
const lessThanTen = arr.filter(num => num < 10);
console.log(lessThanTen);

執行結果

[3, 6, 9]

OK,先不要把重點放在 filter 的 code 比較少這件事,因為如果把 filter 底層的 code 翻出來,執行的量絕對不會少於上面的 for 迴圈。

重點在於,我們抽象化了「篩選」這個動作。

我們抽象化了「篩選」這個動作。

抽象化了「篩選」

系統提示:你看到腦中的回音了

抽象化的意義

抽象化並不是把 for 迴圈拉出去當 function 那麼簡單:

// 篩選出 10 以下的數字
const lessThanTenFilter = (inputArr) => {
    const lessThanTen = [];
    for (let i=0; i<inputArr.length; i++) {
        if (inputArr[i] < 10) {
            lessThanTen.push(inputArr[i]);
        }
    }
    return lessThanTen;
};
const arr = [3, 6, 9, 12, 15];
const result = lessThanTenFilter(arr);
console.log(result);

如果我今天需要篩選的是

  • 5 以下的數字
  • 乘以 3 是 2 的倍數的數字
  • 與今天月份相同的數字

我是不是還要為了這幾個特別的 case,又多寫三個 function 出來?

聽起來就很難維護啊!所以我想抽象化就是為了解決這個問題,可能稱作「客製化」的問題吧!

我們把篩選這個動作抽象化,所有想要做「篩選」動作的,都可以呼叫 filter,然後根據你的需求,把判斷用的 function 丟進參數,就完成一個「客製」的 filter 了。

const arr = [3, 6, 9, 12, 15];

// 篩選出 5 以下的數字
arr.filter(num => num < 5);
// 乘以 3 是 2 的倍數的數字
arr.filter(num => (num * 3) % 2 === 0);
// 與今天月份相同的數字
arr.filter(num => num === new Date().getMonth() + 1);

執行結果

[3]
[6, 12]
[9]

所以或許可以這樣說,HoF 能夠賦予 function 在某個基礎上客製化的能力。

回傳函式的 HoF

比如我們自己來寫一個,回傳 function 的 HoF:

const addNum = (x) => {
  return (y) => {
    return x + y;
  };
};
// 可簡化成
// const addNum = x => y => x + y;

const addFive = addNum(5);
const addTen = addNum(10);

addFive(3); // 8
addTen(3); // 13

有感受到了嗎?透過 addNum 這個 HoF,我們可以很快「客製」出兩個額外的 function,分別處理 +5 與 +10 的 case,這是抽象化非常厲害的地方呢!

如果我說明得不夠清楚,也歡迎大家補充,或者可以看看 Quora 的網友怎麼看

Pure Functions(純函式)

關於純函式的定義,在維基可以看到比較精準的定義:

函式與外界交換資料只有一個唯一渠道——參數和回傳值

  • 函式從函式外部接受的所有輸入資訊,都通過參數傳遞到該函式內部
  • 函式輸出到函式外部的所有資訊,都通過回傳值傳遞到該函式外部

白話一點:

在函式內出現的變數,要嘛是函數內自己宣告的,要嘛是從參數傳進來的,有其他來源的話就是 impure

而 impure 的函式,就代表函式裡面有 side effects。

Side Effects

side effects 我們有在 Day 6 - Function 時空旅行 (1) 提到過,如字面上的意思就是副作用,翻成白話應該是:「你做的事影響到其它人」。

常見的 side effects 如下:

  • 發送 http request (如 fetchaxios)
  • 在畫面印出值或是 log (如 console.log)
  • 操作 DOM 物件 (如 docuement.querySelector)

我想許多人會感到困惑的應該是這個吧。。。

console.log 怎麼也算 side effects 啊!它招誰惹誰了QQ 把東西印出來又不會出事!

這部分算是我也還在理解的,我想是因為 pure function 要的是完全的純粹,也就是這個 function 裡面只要做好它「該做的事」。

而像 console.log 這樣其實是去呼叫 window.console.log 的指令,一來它「不是該做的事」,二來它就是「影響到別人了」。

100% 的純度?

這邊需要強調一點,不用強硬追求 100% 的 pure,或者 100% 沒有 side effects,因為如果真的達到 100% 了,是不是也不能夠發送 http request 跟操作 DOM 了呢?

我認為要追求的是,盡可能讓有 side effects 的程式碼被集中(共用),不要東一個西一個,才能夠將測試時的負擔降到最低。

Pure 追求的不是

zero side effects

而是

minimize side effects

結語

今天介紹了關於 FP 幾個常見的特性,尤其是關於 HoF 的意義,我自己也在查資料的過程中思考了許多,有一些思維其實沒有一定的做法,但總是會在碰到某些困難時,靈光一閃覺得「好像可以這樣用!」,我想這就是學習不同 coding 思維很有趣的地方!

幻化
在空曠的荒野灑落
通向八方的道路

參考資料

hannahpun - Function Programming In JS
Po-Ching Liu - javascript-functional-programming
Why-are-higher-order-functions-important-to-functional-programming


上一篇
Day 7 - Function 時空旅行 (2) - 拆解與命名
下一篇
Day 9 - Functional Programming 初探 (2) - Currying 與 Composition
系列文
Javascript 從寫對到寫好30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
TD
iT邦新手 4 級 ‧ 2021-09-24 10:06:07

console.log 怎麼也算 side effects 啊

這個我也是很納悶,有看到另外一種解釋:How is printing to the console a side effect

看更多先前的回應...收起先前的回應...
Ken Chen iT邦新手 4 級 ‧ 2021-09-25 00:43:58 檢舉

console.log 也算是種操縱外部的行為,你把東西印出到不屬於 function 內、code 以外的地方,瀏覽器開發工具的控制台,或是 node 的朋友終端機介面上

A side effect is when a function relies on, or modifies, something outside its parameters to do something.

Ken Chen iT邦新手 4 級 ‧ 2021-09-25 01:35:08 檢舉
console.log("hello")

可以想成,你的input, argument 是 "hello" ,然後沒有 return 任何東西,但是卻把東西印在外面的控制台(操控外部)

TD iT邦新手 4 級 ‧ 2021-09-25 09:36:56 檢舉

打個岔 > <

有看到 console.log returns undefined。不過是真的 return undefined 還是其實什麼事情都沒有做呢?

ycchiuuuu iT邦新手 4 級 ‧ 2021-09-25 11:35:49 檢舉

謝謝 Ken 的補充,有更強調了 console.log 其實是去操作外部(瀏覽器)的行為,沒有 return statement 本身也不太符合 FP 的風格。

我也補充一下,log 的時候是 return undefined,可以參考這篇,但印象中其實 function 裡面只要沒有 return,就是預設幫你 return undefined。

const a = () => {};
a(); // undefined
Ken Chen iT邦新手 4 級 ‧ 2021-09-25 15:43:24 檢舉

嗯嗯,就像 ycchiuuuu 說的一樣

關於 console 的 return ,console 實際上是 Web API 所以每家瀏覽器底層的實作可能不一樣,但我覺得主要任務應該還是印出東西在主控台,應該大同小異

The specifics of how it works varies from browser to browser... by MDN

另外,function default return 也像ycchiuuuu說的一樣

其實 function 裡面只要沒有 return,就是預設幫你 return undefined。

To return a specific value other than the default, a function must have a return statement ... For all other functions, the default return value is undefined. by MDN

TD iT邦新手 4 級 ‧ 2021-09-25 22:00:38 檢舉

感謝兩位回覆~~

我要留言

立即登入留言