前言
最近在前端面試中遇到的考題中出現了柯里化,一個會讓人出現很多問號的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);
const Fn = add(1); Fn(2);
const curry = a => b => a + b; const add1 = curry(1) add1(2);
|
參數共用的應用
用柯里化的其中一個好處是可以讓共同的參數進行重複使用
例如:要做一個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);
|
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)) console.log(curry(minus)(3)(4))
|
兩組物件,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'));
console.log(curry(getNames)(girlList)('girl'));
|
多組參數延遲輸入的應用
若我們要實現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 = function(){ return args.reduce((a, b)=>a + b); } return inner; }
console.log(add(1, 2, 3)); console.log(add(1)(2,3)); console.log(add(1)(2)(3));
|
別種解法回傳為 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); } 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)); console.log(curriedSum(1)(2,3)); console.log(curriedSum(1)(2)(3));
|
柯里化非同步的應用
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);
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);
|
柯里化的一些觀念和缺點
這邊參考了這篇文章提到的
優
- currying的方式提升了函式的重複使用性。
缺
有其他解决方案。在JS中使用柯里化其實效能上較不好,例如操作dom的事件,但在差異沒有很大,在整個架構上看,差異可以忽略。像是用bind、箭頭函式可以解決。
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);
curried(1, 2)(3);
curried(1, 2, 3);
curried(1)(_, 3)(2);
|
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);
curried(2, 3)(1);
curried(1, 2, 3);
curried(3)(1, _)(2);
|
有興趣的話可看
參考資料