# JavaScript的柯里化 (Currying in JavaScript)
###### tags: `JavaScript` `Interview Preparation`
:::warning
:bulb: 本站筆記已同步更新到我的[個人網站](https://simplydevs.netlify.app/)囉! 歡迎參觀與閱讀,體驗不同的視覺感受!
:::
[TOC]
<img src="https://images.unsplash.com/photo-1631452180539-96aca7d48617?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80">
<div style="text-align:center;font-size: 10px">
(Photo by <a href="https://unsplash.com/@mekalluakella?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Kalyani Akella</a> on <a href="https://unsplash.com/photos/gml9g1kRQcM?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>)
</div>
<br/>
> 此Curry非彼Curry,不過Naan沾咖哩真的好好吃啊:yum:
## :memo: 前言
柯里化(Currying)是functional programming的一種技術 ,透過currying可以編寫模組化、易於測試和高度可重複使用的程式碼。
Functional programming(FP)是一種宣告式規範( declarative paradigm),強調不變性(immutability)和純函式(Pure Functions)—代表該函式對於任何給定的輸入(input),永遠會回傳相同的輸出(output)。這些特性可以使程式碼更易讀並且更容易維護,而Currying只是其中的一種技術。
:::info
更多關於Functional Programming,可以閱讀以下文章:
1. [為什麼要學 Functional Programming?](https://ithelp.ithome.com.tw/articles/10233399)
2. [JavaScript: Functional Programming 函式編程概念](https://totoroliu.medium.com/javascript-functional-programming-%E5%87%BD%E5%BC%8F%E7%B7%A8%E7%A8%8B%E6%A6%82%E5%BF%B5-e8f4e778fc08)
3. [Buzz Word 1 : Declarative vs. Imperative](https://ithelp.ithome.com.tw/articles/10233761)
:::
## :memo: 柯里化(Currying)
### 原理
Currying可以將**具有多個參數的函式轉換為一系列巢狀(nesting)函式**。也就是說,函式並非一次接受所有參數,而是接受第一個參數並返回一個新函式,此新函式接受第二個參數並返回另一個新函式,此新函式再接受第三個參數,依此類推,直到所有參數都被執行完畢。
### 範例
舉例來說,以下函式尚未進行currying:
```javascript=
function multiply(a,b) {
return a * b;
}
```
如果只傳入一個參數`a`,此函式仍會執行,而參數`b`則會以`undefined`去執行,也就是說如果執行`multiply(5)`,實際上執行的是`5 * undefined`,回傳`NaN`。
現在試著將函式currying,轉換成一系列巢狀函式:
```javascript=
function curried_multiply(a) {
return function nested(b) {
return a * b;
}
}
```
調用`curried_multiply()`函式:
1. 調用`curried_multiply()`函式,這個函式接受一個參數傳入,並且回傳另一個函式`nested()`。也就是**調用`curried_multiply(a)`,回傳的結果是`nested(b)`。**
2. 調用`nested()`函式,這個函式使用調用`curried_multiply()`以及`nested()`函式得到的參數`a`、`b`,並執行與回傳`a * b`的結果。**調用`nested(b)`,回傳的結果是`a * b`。**
如果進一步拆解這個函式:
```javascript=
const one = curried_multiply(5)
console.log(one)
// output: [Function: nested]
```
回傳的值是`nested()`函式,接著呼叫`one()`函式,實際上是執行`nested()`函式:
```javascript=
one(10)
// output: 50
```
回傳的是 `a * b`,也就是 `5 * 10`的值。
### 閉包(closure)與柯里化(currying)
呼叫`curried_multiply()`函式時調用的參數可用於巢狀函式,這是**閉包(closure)的特性**。函式中回傳函式,通常就是閉包。 當呼叫父函式時,會產生一個新的執行環境(context),這個環境會保留所有區域變數(local variables),這些區域變數可以透過和全域變數連結、或是從父函式的閉包,在全域環境中被取用。例如以下函式:
```javascript=
function foo(x) {
function bar(y) {
console.log(x + y);
}
bar(2);
}
foo(2)
// output: 4
```
`x`被綁定在外部函式`foo()`中,當執行內部函式`bar()`時,`bar()`可以取用`x`,因為`bar()`是在`foo()`的作用域中建立的,父函式`foo()`執行完後,變數`x`被儲存於閉包中,根據JS的[Garbage Collection機制](https://www.geeksforgeeks.org/relation-of-garbage-collector-and-closure-in-javascript/),執行`bar()`時找到其中有參照變數`a`,因此`a`不會被清除掉。另一方面,`bar()`可以取用其父函式及全域的變數,但是如果`bar()`中宣告其它函式、或`foo()`中其他函式的作用域(也就是平行於`bar()`函式)的變數,則`bar()`不可取用這些函式的變數。
:::info
Q: 函式中的函式一定代表閉包的存在?
A: 不一定。如果內部的函式(子函式)並沒有參照該函式作用域外(如父函式作用域)的變數,則閉包不存在。例如:
```javascript=
function foo(x) {
return function () {
return true
}
}
foo(5)()
//output: true
```
以上例子中,不管傳入任何值到`foo()`,回傳的永遠是true,因為內部的函式並沒有參照到`foo()`作用域的變數`x`,這種狀況下閉包不存在。
:::
巢狀函式根據定義函式的位置,保留父函式的作用域;亦即,內層函式的區塊也可以取用外層函式的變數,例如前述[範例](###範例)中,`function nested(b) {return a * b}`,`nested()`函式可以取用父函式、也就是`curried_multiply()`函式的參數`a`,當我們執行`const one = curried_multiply(5)`時,`one()`保留了`curried_multiply()`的作用域,因此可以取用該作用域的變數`5`;也可以理解成在此巢狀函式中,第一個傳入的參數`a`,會成為閉包中的變數被記憶/儲存,並傳入巢狀函式鏈中的下一個函式執行。
### 箭頭函式
使用ES6的箭頭函式語法,重新撰寫前述[範例](###範例)中的`curried_multiply()`函式:
`let curried_multiply = a => b => a * b`
拆解一下上面這行程式碼:
1. 把最外層的箭頭函式`a => ...`賦值給變數`curried_multiply`
(i.e. `curried_multiply`是一個接受`a`作為參數傳入的函式 )
3. 呼叫`curried_multiply`,傳入參數`a`,回傳另一個箭頭函式`b => a * b`(i.e. `curried_multiply`函式接受`a`作為參數傳入後,回傳的函式接受`b`作為參數傳入 )
4. 調用箭頭函式`b => a * b`,回傳相乘的結果(`a * b`)
試著將參數傳入這個函式:
```javascript=
let newNum = curried_multiply(2)(8)
console.log(newNum)
// output: 16
```
### 進階Currying範例
```javascript=
const curry =(fn) =>{
return curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args)
}
}
```
以上code做了什麼?
1. `curry`作為外層函式,接受`fn`函式作為參數傳入,並回傳另一個函式`curried`
2. `curried`接受另一參數`args`傳入,並用[其餘參數(rest parameters)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters)將參數集合成陣列,並且比較`fn`和`args`的長度
:::warning
:bulb: 函式的長度(function length)是function的一種屬性(property),**表示該 function 預期被傳入的參數數量**,這個數量並不包含其餘參數(rest parameter)且只包含第一個預設參數(Default Parameters)前的參數。
*Ref: [MDN doc](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length)*
:::
3. `if`判斷式中的邏輯: 若`fn`和`args`的長度(也就是參數的數量)不同,則呼叫[`bind()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)方法,建立一個新的函式並傳入參數`...args`;若`fn`和`args`的長度(也就是預期傳入的參數數量)相同,則回傳傳入`args`參數的`fn`
:::warning
:bulb: `bind()`是函式的一種方法(method),它的基本語法如下:
`fun.bind(thisArg[, arg1[, arg2[, ...]]])`
第一個參數是`this`要指向的物件,第二個與其後的參數則是要傳入該函式的參數。`bind()`會建立一個新的函式,必須調用該函式才會執行。
:::
實際應用這個函式:
```javascript=
const totalNum=(x,y,z) => {
return x+y+z
}
const curriedTotal = curry(totalNum)
console.log(curriedTotal(10)(20)(30))
// output: 60
```
讓我們更進一步的觀察這個函式做了什麼,在`curry`函式中加入一些code:
```javascript=
const curry =(fn) =>{
return curried = (...args) => {
// 把args和fn相關資訊印出來
console.log(args)
console.log(fn.length)
console.log(args.length)
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args)
}
}
```
再執行一次剛剛的`curriedTotal(10)(20)(30)`函式,在console上印出:
```javascript=
// first round
[ 10 ]
3
1
// second round
[ 10, 20 ]
3
2
// third round
[ 10, 20, 30 ]
3
3
// sum output
60
```
印出的順序依序是<span style="background-color: tan">`args`</span>、<span style="background-color: lightcoral">`fn.length`</span>、<span style="background-color: gold">`args.length`</span>,因此console印出的東西可以分成三組:
1. 傳入`curry`函式的參數是`totalNum`函式,這個函式有三個參數,因此<span style="background-color: lightcoral">`fn.length`</span>是3
2. 第一次傳入一個參數,使用其餘參數轉換成陣列 `[10]`,也就是
<span style="background-color: tan">`args`</span>,此時的<span style="background-color: gold">`args.length`</span>是1,和<span style="background-color: lightcoral">`fn.length`</span>不同,因此執行`curried.bind(null, ...args)`,`curried`的參數數量變成1
3. 第二次也是傳入一個參數20,和剛剛的參數10使用其餘參數轉換成陣列 `[10, 20]`,也就是
<span style="background-color: tan">`args`</span>,此時的<span style="background-color: gold">`args.length`</span>是2,和<span style="background-color: lightcoral">`fn.length`</span>不同,因此執行`curried.bind(null, ...args)`,`curried`的參數數量變成2
4. 第三次傳入參數30,此時的<span style="background-color: tan">`args`</span>是 `[10, 20, 30]`,<span style="background-color: gold">`args.length`</span>是3,和<span style="background-color: lightcoral">`fn.length`</span>相同,因此執行`fn(...args)`,也就是把`[10, 20, 30]`使用[展開運算子(spread operator)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax)傳入`totalNum`函式,加總後得到結果60。
假如不使用currying的函式,一次傳入三個參數`curriedTotal(10, 20, 30)`,則會在console上印出:
```javascript=
[ 10, 20, 30 ]
3
3
60
```
因為傳入的參數和`totalNum`參數數量相同,因此直接執行`fn(...args)`。
以上範例的`curry`函式也可以用[`apply()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)方法執行,結果是一樣的:
```javascript=
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(null, args);
} else {
return function(...args2) {
return curried.apply(null, args.concat(args2));
}
}
}
}
```
與`bind()`方法的不同之處在於,`apply()`方法接受的第二個參數是一個**陣列**,因此無須再展開。
:::warning
:bulb: [`apply()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply)方法的基本語法:
`fun.apply(thisArg, [argsArray])`
第一個參數一樣是`this`指向的對象,第二個參數則是參數陣列或是array-like 物件;和`bind()`方法不一樣,`apply()`是直接執行該函式,而非建立新函式(或是說拷貝原函式物件)。
:::
:::info
:bookmark: 更多關於`apply()`、`bind()`、`call()`方法,可以閱讀以下文章:
- [使用 bind、call、apply 改變 this 指向的對象](https://b-l-u-e-b-e-r-r-y.github.io/post/BindCallApply/)
- [JavaScript 中 call()、apply()、bind() 的用法](https://www.runoob.com/w3cnote/js-call-apply-bind.html)
- [[JavaScript] 函數原型最實用的 3 個方法 — call、apply、bind](https://realdennis.medium.com/javascript-%E8%81%8A%E8%81%8Acall-apply-bind%E7%9A%84%E5%B7%AE%E7%95%B0%E8%88%87%E7%9B%B8%E4%BC%BC%E4%B9%8B%E8%99%95-2f82a4b4dd66)
:::
## :memo: Currying的優點
Currying可以**使函式具有單一用途,使程式碼更加模組化,從而更易於測試、debug、維護和閱讀。** 以下直接用範例說明:
假如我們有以下的資料,想要進行篩選與排序:
```javascript=
const candidate = [
{ age: 20, location: "CA", skills:"JavaScript", dateApplied: new Date('2021-01-20') },
{ age: 26, location: "TX", skills:"Python", dateApplied: new Date('2022-11-09') },
{ age: 45, location: "OR", skills:"PHP", dateApplied: new Date('2021-03-06') },
{ age: 26, location: "NJ", skills:"JavaScript", dateApplied: new Date('2019-12-30') },
{ age: 33, location: "LA", skills:"Python", dateApplied: new Date('2020-05-10') },
{ age: 18, location: "CA", skills:"JavaScript", dateApplied: new Date('2018-07-17') },
{ age: 31, location: "NJ", skills:"Java", dateApplied: new Date('2022-05-18') }
]
```
針對`candidate`資料**根據地點進行篩選並排序**,因此寫了以下的函式:
```javascript=
const sortByValueFromLocation = (candidateArr, location, sortKey) => {
return candidateArr.filter(candidate => {
return candidate.location === location
}).sort((a,b) => {
return a[sortKey] - b[sortKey]
})
}
console.log(sortByValueFromLocation(candidate, "CA", "age"))
```
假設今天需要另一個兩個篩選條件的函式來處理這筆資料,於是我們又寫了另一個函式:
```javascript=
const filterByValueFromLocation = (candidateArr, location, filterKey, filterValue) => {
return candidateArr.filter(candidate => {
return candidate.location === location
}).filter(candidateFromCity => candidateFromCity[filterKey] === filterValue)
}
console.log(filterByValueFromLocation(candidate, "CA", "skills", "JavaScript"));
```
以上兩個函式可以觀察到:
1. 這兩個函式都根據地點篩選,並且每次都執行相同的篩選方式: 寫了許多重複的code
2. 這兩個函式都接受多個參數: 倘若發生錯誤,需要花比較多時間確認是哪個參數造成的錯誤
試著用Currying改寫以上函式,讓code變得更模組化、容易閱讀、可以重複使用:
```javascript=
const setFilter = array => key => value => array.filter(x => x[key] === value);
const filterCandidates = setFilter(candidate);
const filterCandidatesByLocation = filterCandidates('location');
const filterCandidatesByCA = filterCandidatesByLocation('CA');
const filterCandidatesBySkills = filterCandidates('skills');
const filterCandidatesByJavaScript = filterCandidatesBySkills('JavaScript');
console.log(filterCandidatesByCA)
// 回傳地點是CA的人選
console.log(filterCandidatesByJavaScript);
// 回傳技能是JavaScript的人選
```
code模組化後,可以重用於其他功能,例如可以將上述技能是JavaScript的人選排序:
```javascript=
const sortArrayByValue = sortArray => sortKey => {
return sortArray.sort(function(a, b){
if(a[sortKey] < b[sortKey]) { return -1; }
if(a[sortKey] > b[sortKey]) { return 1; }
return 0;
});
}
const sortJS = sortArrayByValue(filterCandidatesByJavaScript);
const sortJSByDate = sortJS("dateApplied");
console.log(sortJSByDate)
/* output:
[
{
age: 18,
location: 'CA',
skills: 'JavaScript',
dateApplied: 2018-07-17T00:00:00.000Z
},
{
age: 26,
location: 'NJ',
skills: 'JavaScript',
dateApplied: 2019-12-30T00:00:00.000Z
},
{
age: 20,
location: 'CA',
skills: 'JavaScript',
dateApplied: 2021-01-20T00:00:00.000Z
}
]
*/
```
上面的程式碼中,`filterCandidatesByJavaScript`作為`sortArrayByValue`的`sortArray`參數傳入,並回傳`sortKey => ...` 這個函式,接著再傳入`"dateApplied"`作為`sortKey`參數執行`sortArray`.[`sort()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)函式,最後回傳排序結果。
## :memo: Partial application vs. Currying
### 什麼是Partial Application
所謂的application,是指將函式應用(apply)在其參數(argument)上,以產出一個回傳值的過程;所以,partial application就是將該函式應用於其**部分參數**的過程,而該函式回傳留作它用。換句話說,partial application就是**一個函式接受多個參數、並回傳一個含有較原先參數數量少的函式的過程**。**和currying一樣,都會應用到閉包的概念,差別在於currying一次只傳入一個參數。** partial application將一或多個參數值固定在回傳的函式中,該回傳函式接受剩餘的參數,以便能完整執行該函式。聽起來好像很奇怪,不過這樣的做法可以減少參數的數量。請看以下範例:
```javascript=
const promotionDetails = (product, discount, startDate) => {
return `The ${product} price is ${discount}% off from ${startDate} `
}
const fruitPromotion = startDate => {
return promotionDetails('fruits', 20, startDate)
}
const dairyPromotion = startDate => {
return promotionDetails('dairy products', 20, startDate)
}
```
假設今天要做一個商品打折的活動,我們先設定了一個通用的`promotionDetails`函式,需要傳入三個參數`product`, `discount`, `startDate`,接著當我們需要分別對各種商品進行處理,又寫了幾個不同的函式,只是...怎麼每個看起來都好像,好多重複的code!試著改寫一下:
```javascript=
const promotionDetails = (discount, product, startDate) => {
return console.log(`The ${product} price is ${discount}% off from ${startDate} `)
}
const twentyOffPromotion = (product, startDate) => {
return promotionDetails(20, product, startDate)
}
const fruitPromotion = startDate => {
return twentyOffPromotion('fruits', startDate)
}
const dairyPromotion = startDate => {
return twentyOffPromotion('dairy products', startDate)
}
```
看起來是不是比較簡潔?把共通的部分抽取出來,也就是折扣20%,另外寫成函式`twentyOffPromotion`,並且把它的參數`discount`固定為20這個值,便可以把這個函式應用在其他的函式中,`fruitPromotion`和`dairyPromotion`都只需要接收參數`startDate`即可。這就是partial application。
### 高階函式 (High Order Function)
高階函式意指一個函式可以接收另一函式作為參數、或者回傳一個函式。
:::info
Ref: [高階函式 (Higher Order Function) 是什麼?](https://www.explainthis.io/zh-hant/swe/what-is-hof)
:::
上述的範例如果應用高階函式改寫:
```javascript=
const partial = (fn, ...argsToApply) => {
return (...restArgsToApply) => {
return fn(...argsToApply, ...restArgsToApply)
}
}
const promotionDetails = ( discount, product, startDate) => {
return console.log(`The ${product} price is ${discount}% off from ${startDate} `)
}
const twentyOffPromotion = partial(promotionDetails, 20)
const fruitPromotion = partial(twentyOffPromotion, 'fruits', '2020/02/20')
fruitPromotion() // The fruits price is 20% off from 2020/02/20
const dairyPromotion = partial(twentyOffPromotion, 'dairy products', '2020/01/23')
dauryPromotion() // The dairy products price is 20% off from 2020/01/23
```
`partial`函式做了什麼?
1. 傳入兩個參數`fn`, `argsToApply`:`fn`是要被partially applied的函式,`argsToApply`則是收集傳入的參數,利用其餘參數轉換為陣列
2. 執行`partial`函式會回傳另一個函式,這個函式接收除了 `argsToApply`以外的參數,這裡命名為 `restArgsToApply`
3. 執行這個回傳的函式會再回傳另一個函式,這個函式接收兩個參數`argsToApply`及 `restArgsToApply`;這裡跟currying一樣,應用到閉包的概念,執行外層`partial`函式的參數`argsToApply`和`fn`儲存於閉包中,內部的函式因此可以取得外部函式(父函式)的參數。所以最內層回傳的函式,其實是執行最外層傳入的參數`fn`,也就是我們要進行partially applied的函式。
---
## 參考資料
* MDN文件
* [Closures & Currying in JavaScript](https://engineering.cerner.com/blog/closures-and-currying-in-javascript/)
* [[JS] Functional Programming and Currying](https://pjchender.dev/javascript/js-functional-programming-currying/#%E4%BB%80%E9%BA%BC%E6%98%AF-currying)
* [Understanding JavaScript currying](https://blog.logrocket.com/understanding-javascript-currying/)
* [Day 22 :什麼是 Currying(4)?自己動手寫一個 Curry 吧!](https://ithelp.ithome.com.tw/articles/10297474)
* [進階 Javasctipt 概念 (4)](https://medium.com/@paulyang1234/%E9%80%B2%E9%9A%8E-javasctipt-%E6%A6%82%E5%BF%B5-4-703e69416118)
* [Closure 閉包](https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/closure.html)
* [JavaScript 深入淺出 Garbage Collection 垃圾回收機制](https://shawnlin0201.github.io/JavaScript/JavaScript-Garbage-Collection/)
* [Day02_JS的二三事](https://ithelp.ithome.com.tw/articles/10192743)
* [Beginners Guide To Higher Order Functions, Partial Functions and Currying](https://dev.to/ubahthebuilder/beginners-guide-to-higher-order-functions-partial-functions-and-currying-2dm5#:~:text=A%20higher%20order%20function%20is%20a%20function%20which%20returns%20another,until%20it%20is%20fully%20resolved.)
* [Functional JS #5: Partial Application, Currying](https://medium.com/dailyjs/functional-js-5-partial-application-currying-da30da4e0cc3)
* [Good Morning, Functional JS (Day 10, Partial Application 偏函數應用)](https://ithelp.ithome.com.tw/articles/10194837)
* Codecademy 教材
::: success
:crescent_moon: 本站內容僅為個人學習記錄,如有錯誤歡迎留言告知、交流討論!
:::