[Functional JS] Currying 柯里化 & Partial application

[Functional JS] Currying 柯里化 & Partial application

前言

最近在前端面試中遇到的考題中出現了柯里化,一個會讓人出現很多問號的js高階用法,一直是我沒有很熟悉高階js、functional programming這一塊,因為不是資訊科系出身,深感還有好多不足(嘆…),希望可以趕快補足。在研究了解柯里化,就會發現其中的好處,也可以應用在實際案例中。

柯里化 Currying

柯里化 Currying 可以讓呼叫函式 f(a, b, c) 轉換成可以分開呼叫 f(a)(b)(c)。原理是透過部分的參數呼叫一個 function,它會回傳一個 function 去處理剩下的參數。可應用在 參數共用 和 延遲執行 等。之所以叫做curry是提出這個概念者的名字 Haskell Curry。

Partial application

類似Currying但可以接收多個參數,currying是一次一個,函式 f(a, b, c) 轉換成可以分開呼叫 f(a)(b, c)

基礎寫法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const add = function(a) {
return function(b) {
return a + b;
};
};

//一次性呼叫
add(1)(2); // 3

//也可以每次只傳遞一個參數
const Fn = add(1);
Fn(2); // 3


const curry = a => b => a + b;
const add1 = curry(1)
add1(2); //3

參數共用的應用

用柯里化的其中一個好處是可以讓共同的參數進行重複使用
例如:要做一個url,共用到https://

1
2
3
4
5
6
7
8
9
const urlCurrying = function(protocol) {
return function(hostname, pathname) {
return `${protocol}${hostname}${pathname}`
};
};

const urlHttps = urlCurrying('https://');
const url = urlHttps('www.abc.com','/123');
console.log(url); // https://www.abc.com/123
1
2
3
4
5
6
const curry = f => a => b => f(a, b);
const add = (a, b) => a + b;
const minus = (a, b) => a - b;

console.log(curry(add)(3)(4)) // 7
console.log(curry(minus)(3)(4)) // -1

兩組物件,key值不同,但想取兩個物件裡的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const girlList = [
{girl:'May'},
{girl:'Amy'},
{girl:'Tina'},
{girl:'Gina'},
];
const boyList = [
{boy:'Kevin'},
{boy:'Lyon'},
{boy:'Henry'},
{boy:'Pete'},
];

const curry = name => element => element[name];
console.log(boyList.map(curry('boy')));
console.log(girlList.map(curry('girl')));


const curry = f => a => b => f(a, b);
const getNames = (a, b) => {
return a.map((i)=>i[b])
};
console.log(curry(getNames)(boyList)('boy'));
// ["Kevin", "Lyon", "Henry", "Pete"]
console.log(curry(getNames)(girlList)('girl'));
// ["May", "Amy", "Tina", "Gina"]

多組參數延遲輸入的應用

若我們要實現f(a, b, c)、f(a)(b)(c)、f(a,b)(c)等多組不同參數帶入方式,但return出同樣的結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function add() {
const args = [...arguments];
const inner = function() {
args.push(...arguments);
return inner;
} //自己調用自己來實現重複輸入參數

//回傳的inner函式呈現方式是函式字串,
//所以可以透過調用toString這個方法來實現回傳的結果。也叫做反柯里化
//反柯里化:透過隱式調用valueOf和toString方式結束延後執行
inner.toString = function(){
return args.reduce((a, b)=>a + b);
}
return inner;
}

//但實際上回傳的值是function顯示成字串但typeOf去查看是function
console.log(add(1, 2, 3)); // 6
console.log(add(1)(2,3)); // 6
console.log(add(1)(2)(3)); // 6

別種解法回傳為 number,但無法帶入超過的參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
//當args陣列中的參數數量大於func函式的參數數量則直接執行func
//若不成立的話則把參數和在一起再調用function curried
} else {
return function(...args2) {
return curried.apply(this, [...args,...args2]);
}
}
}
}

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

let curriedSum = curry(add);

console.log(curriedSum(1, 2, 3)); // 6,args.length = func.length 正常調用
console.log(curriedSum(1)(2,3)); // 6,第一個參數先柯理化
console.log(curriedSum(1)(2)(3)); // 6,所有參數柯里化

柯里化非同步的應用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function fetchData(url, handler) {
const xhr = new XMLHttpRequest();

xhr.open('GET', url, false);
xhr.send(null);
let result = JSON.parse(xhr.responseText);
handler(result);
}

function showResult(result) {
console.log(result);
console.log(`${result[0].name.first}`);
}

const url = 'https://next.json-generator.com/api/json/get/EkcNtgkpY';
fetchData(url, showResult);

//使用currying後,寫法更簡便

function curriedFetchData(url) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.send(null);
let result = JSON.parse(xhr.responseText);

return function(_callback) {
_callback(result);
}
}

function showResult(result) {
console.log(result);
console.log(`${result[0].name.first}`);
}

const url1 = 'https://next.json-generator.com/api/json/get/EkcNtgkpY';
const url2 = 'https://next.json-generator.com/api/json/get/NJB_fbk6F';

const getData1 = curriedFetchData(url1);
const getData2 = curriedFetchData(url2);
getData1(showResult);
getData2(showResult);

柯里化的一些觀念和缺點

這邊參考了這篇文章提到的

  1. currying的方式提升了函式的重複使用性。

  1. 有其他解决方案。在JS中使用柯里化其實效能上較不好,例如操作dom的事件,但在差異沒有很大,在整個架構上看,差異可以忽略。像是用bind、箭頭函式可以解決。

  2. currying是函數式的程式概念,若還沒準備好寫純正的函數式程式,有更好的替代方案,例如在JSX中綁定一次共同參數用bind或箭頭函式即可。JavaScript 並不是真正的函數式程式语言,相比 Haskell 等函數式程式语言,JavaScript 使用 Currying 等函數式特性有額外的性能開銷,也缺乏類型推導。把js寫的符合函數式程式的思想和規範的項目比較少,也限制了currying等技術在js中普遍使用。

lodash

lodash提供了_.curry和_.curryRight(參數相反)的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var abc = function(a, b, c) {
return [a, b, c];
};

var curried = _.curry(abc);

curried(1)(2)(3);
// => [1, 2, 3]

curried(1, 2)(3);
// => [1, 2, 3]

curried(1, 2, 3);
// => [1, 2, 3]

// Curried with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var abc = function(a, b, c) {
return [a, b, c];
};

var curried = _.curryRight(abc);

curried(3)(2)(1);
// => [1, 2, 3]

curried(2, 3)(1);
// => [1, 2, 3]

curried(1, 2, 3);
// => [1, 2, 3]

// Curried with placeholders.
curried(3)(1, _)(2);
// => [1, 2, 3]

有興趣的話可看

參考資料

[Functional JS] Currying 柯里化 & Partial application

https://kaiyuncheng.github.io/2020/12/25/currying/

Author

KaiYun Cheng

Posted on

2020-12-25

Updated on

2024-04-13

Licensed under

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×