# Can you give an example of a curry function and why this syntax offers an advantage?
# 定義
Currying是一種模式,即一個有多個參數的函數被分解成多個函數,這些函數在被串聯調用時,將逐個累積所有需要的參數。
這種技術對於使以函數式風格編寫的代碼更容易閱讀和編排非常有用。需要注意的是,要使一個函數被curried,它需要從一個函數開始,然後分解成一連串的函數,每個函數接受一個參數。
> 會使用到閉包跟遞迴的概念。
# 優點
這樣做的好處是
- 簡化參數的處理,基本上是一次處理一個參數,藉以提高程式的彈性和可讀性
- 將程式碼依功能拆解成更細的片段,有助於重複利用
# 範例
1. 把 `multiply(a,b)` 改寫成 `multiply(a)(b)`
```jsx=
function multiply(x, y){
return x * y;
}
console.log(multiply(3, 5)); // 15
/* -------改寫後-------- */
function curriedMultiply(x) {
return function(y) {
return x * y;
}
}
console.log(curriedMultiply(3)(5)); //15
/* ---- 應用柯里化後的 function ---- */
const multipleThree = curriedMultiply(3);
multipleThree(5); // 15
multipleThree(10); // 30
```
2. 經典面試題
> 實現一個sum方法,使計算結果能夠滿足如下預期:
sum(1)(2)(3) = 6;
sum(1, 2, 3)(4) = 10;
sum(1)(2)(3)(4)(5) = 15;
```jsx=
// 不限數量參數
function sum() {
// 第一次執行時,定義一個數組專門用來存儲所有的參數
var _args = Array.prototype.slice.call(arguments);
// 在內部聲明一個函數,利用閉包的特性保存_args並收集所有的參數值
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// 利用toString隱式轉換的特性,當最後執行時隱式轉換,併計算最終的值返回
_adder.toString = function () {
let sum = _args.reduce(function (a, b) {
return a + b;
});
return sum
}
return _adder;
}
console.log(sum(1)(2)(3)) // 6
console.log(sum(1, 2, 3)(4)) // 10
console.log(sum(1)(2)(3)(4)(5)) // 15
```
## ES6
```jsx=
function sumWithES6(...rest) {
var _args = rest;
var _adder = function (...innerRest) {
_args.push(...innerRest); // 这里使用的是ES6数组的解构
return _adder;
};
_adder.toString = function () {
let sum = _args.reduce(function (a, b) {
return a + b;
});
return sum;
};
return _adder;
}
console.log(sumWithES6(1)(2)(3)); // 6
```
**Arguments物件**
arguments 物件是一個對應傳入函式之引數的類陣列(Array-like)物件。
```jsx=
function objArgs(){
console.log(arguments);
}
console.log(objArgs(1));
/*
[object Arguments] {
0: 1
}
*/
console.log(objArgs(1,2,3));
/*
[object Arguments] {
0: 1,
1: 2,
2: 3
}
*/
```
3. 面試題目的範例:自動柯里化
```jsx=
function curry(fn) {
if (fn.length === 0) {
return fn;
}
function _curried(depth, args) {
return function (newArgument) {
if (depth - 1 === 0) {
return fn(...args, newArgument);
}
return _curried(depth - 1, [...args, newArgument]);//addFive本人
};
}
return _curried(fn.length, []); //curriedAdd本人
}
function add(a, b) {
return a + b;
}
var curriedAdd = curry(add);
var addFive = curriedAdd(5);
var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
console.log(result);
```
步驟如下:
1. 建立 `addFive`
```jsx=
//addFive應該要是什麼內容:(x)=>(x+5)
const addFive = function(x){
return x+5; //這個 5 應該要是從某個 function 中 assign的值
}
var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
console.log(result);
```
2. 把 `addFive` 指定為另一個 function return 的值:`curriedAdd`
```jsx=
//addFive應該要是什麼內容:(x)=>(x+5)
const curriedAdd = function(y){
return x+y; //y = 5
}
const addFive = curriedAdd(5);
var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
console.log(result);
```
:::info
`addFive` 和 `curriedAdd` 的型態完全一樣(接收一個參數,返回兩個值的總和)。
:::
3. 接下來要想辦法拿到 x 的值,有兩種做法:
a. 直覺式的柯里化:和第一個範例寫法相同
```jsx=
const curriedAdd = function(y){
return function(x){
return x + y; //y = 5
}
}
const addFive = curriedAdd(5);
/*
(x)=>(x+5)
*/
var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
console.log(result);
```
:::warning
缺點是當參數數量改變時,要手動新增 function 內 return 的內容。
:::
b. 自動柯里化:和第二個範例寫法類似
> step1: 我們需要一個 curry 函數
> step2: 它接受一個待柯里化的函數為參數
> step3: 返回一個用於接收一個參數的函數
> step4: 接收到的參數放到一個列表中
> step5: 當參數數量足夠時,執行原函數並返回結果
```jsx=
function curry(fn){ //step1: 我們需要一個 curry 函數
//step2: 它接受一個待柯里化的函數為參數
if (fn.length === 0) {
return fn;
}
function _curried(length, args){
return function(newArgument){ //step3: 返回一個用於接收一個參數的函數
if (length > 1){
return _curried(length - 1, [newArgument, ...args]);
}
return fn(...args, newArgument); //step5: 當參數數量足夠時,執行原函數並返回結果
}
}
return _curried(fn.length, []); //step4: 接收到的參數放到一個列表中
}
const add = function(x, y){ //step2: 待柯里化的函數
return x+y;
}
const curriedAdd = curry(add);
const addFive = curriedAdd(5);
var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
console.log(result);
```
### 其他使用自動柯里化的範例
```jsx=
function curry(fn){
if (fn.length === 0) {
return fn;
}
function _curried(length, args){
return function(newArgument){
if (length > 1){
return _curried(length - 1, [...args, newArgument]);
}
return fn(...args, newArgument);
}
}
return _curried(fn.length, []);
}
//examples
const plus = curry((a, b, c, d, e) => a + b + c + d - e);
console.log(plus(1)(2)(3)(4)(5));
const calculate = curry((a, b, c) => a + b + c);
console.log(calculate(3)(4)(5));
const multiple = curry((a, b) => a * b);
console.log(multiple(3)(4));
```
## 參考資料
[Arguments 物件 - JavaScript | MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Functions/arguments)
[JavaScript函数柯里化 - Web前端工程师面试题讲解](https://www.youtube.com/watch?v=9vCUjwPsocs)
[Currying in JavaScript(柯里化) | Summer。桑莫。夏天](https://cythilya.github.io/2017/02/27/currying-in-javascript/)
[面试题-sum(1,2)(3)函数柯里化](https://www.jianshu.com/p/54a532c2f556)
[JS中的柯里化 及 精巧的自动柯里化实现](https://segmentfault.com/a/1190000012364000)