簡單趣談 Functional Programming in JavaScript 之 Currying

簡單趣談 Functional Programming in JavaScript 之 Currying

前言

Functional Programming 設計模式中,還有一個很特別的概念,就叫做 Currying(柯里化),所以這一篇文章就會來介紹一下這個東西。

Currying 是什麼?

首先 Currying 中文有很多種翻譯,像是柯里化、卡瑞化或加里化,這些都是泛指 Functional Programming 設計模式中 Currying 這個概念。

那麼 Currying 究竟有什麼特別的呢?如果用簡單一句話來形容的話,那就是…

Currying 是一個將多個參數的函式,轉換成一次只接受一個參數的函式,並且返回結果的一個過程。

可能有些人會覺得 Currying 是不是用了什麼新的語法,但實際上它只是善加利用了 JavaScript 的特性而已,也就是你所熟悉的 Closure Function 而已。

Currying 的實作

接下來我們當然要來看看 Currying 的實作,首先我們先來看下方的程式碼:

1
2
3
function add(a, b, c) {
return a + b + c;
}

我們可以看到我們宣告了一個名為 add 的函式,這個函式接受三個參數,並且返回這三個參數的總和

Function Add

看似沒問題對吧?但是當你使用這個函式時,若不小心少傳入一個參數,那麼就會出現 NaN 的情況,例如:

1
add(1, 2); // NaN

Note
這邊的 NaN 是因為 1 + 2 + undefined 會得到 NaN

雖然你可能會認為…

「只要替函式預設參數就好了啊?」

確實,這也是一種解決問題的方式,但是這樣子的話,你就必須要知道你的函式會接受多少個參數,因此每次呼叫函式的時候,都必須要傳入所有的參數,那麼這時候就有可能會漏掉導致出現 NaN 的情況。

這時候我們就可以來講講 Currying 了,我們可以將上述的 add 函式改寫成以下:

1
2
3
4
5
6
7
function curryAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}

很有趣吧?我們將原本接受三個參數的函式,改寫成了三個接受一個參數的函式,最後回傳結果。

當我們使用時,只需要這樣寫就可以了:

1
curryAdd(1)(2)(3); // 6

有點難理解這一段的運作對吧?我們來看看這一段的運作流程:

  • 第一次呼叫 curryAdd(1),這時候會回傳一個函式,並且將 a 設定為 1,也就是 function (b) { return function (c) { return 1 + b + c; }; }
  • 第二次呼叫 curryAdd(1)(2),這時候會回傳一個函式,並且將 b 設定為 2,也就是 function (c) { return 1 + 2 + c; }
  • 第三次呼叫 curryAdd(1)(2)(3),這時候會回傳一個函式,並且將 c 設定為 3,也就是 1 + 2 + 3,最後回傳 6

好像沒感受到好處吧?前面有提到 Currying 其實是利用了 Closure Function 的特性,那麼透過這特性,我們可以將變數的狀態保存起來,所以我們就可以設定所謂的「預設參數」,因此就可以這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function curryAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}

const addOne = curryAdd(1);
const addTwo = addOne(2);

// ...過了一千行之後

addTwo(3); // 6

你會發現我們的 ab 都已經被設定好了,這樣子就不用擔心會漏掉參數的問題,我們只需要專注於傳入最後一個參數就好了。

當然你也不一定要拆成這樣,你也可以預設寫死兩個參數,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function curryAdd(a) {
return function (b) {
return function (c) {
return a + b + c;
};
};
}

const addOne = curryAdd(1)(2);

// ...過了一千行之後

addOne(3); // 6

那麼上面是一個很常見的 Currying 基本教學說明,但是實際上 Currying 還有很多種寫法,例如:我們可以使用 ES6 的箭頭函式來實作 Currying,像是這樣:

1
const curryAdd = (a) => (b) => (c) => a + b + c;

(謎之聲:通常新手第一次看到這一段會覺得很難懂,但是其實這跟上面的寫法是一樣的,只是寫法不同而已。)

那麼 Currying 還可以用在何種場景呢?假設我們今天有兩個物件,分別是 userproduct,而這兩個物件都有一個 id 屬性,那麼我們可以這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
const user = {
id: 1,
name: 'Ray',
};

const product = {
id: 1,
name: 'Apple',
};

const isSameId = (a) => (b) => a.id === b.id;

isSameId(user)(product); // true

又或者是用來做物件取值,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const data = {
user: {
id: 1,
name: 'Ray',
},
product: {
id: 1,
name: 'Apple',
},
};

const get = (key) => (obj) => obj[key];

const getUserName = get('name');

getUserName(data.user); // Ray
getUserName(data.product); // Apple

那麼以上就是一個比較簡單的 Currying 概念實作,實際上會不會很常用在實戰上呢?這答案倒是不一定因為 Currying 這個技巧滿看公司開發團隊的,畢竟你剛開始看 const curryAdd = (a) => (b) => (c) => a + b + c; 時,也會有一點 WTF 的感覺,但實際上拆開來看你會發現這其實就是一個很簡單的閉包函式而已。

最後也提一下,也有一些 Library 會使用 Currying 來實作,例如:Ramdalodash/fp 等等,這些 Library 就有使用 Currying 來實作,因此你可以參考看看這些 Library 的寫法唷~

那麼這一篇就先到這邊告一個段落,希望你可以對 Currying 有一個初步的認識,也可以在適當的時候嘗試使用 Currying 來實作練習看看唷~

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ