最近在學習函數語言程式設計,看到柯里化函式這個東東,原以為是個新的概念,沒想到一查,竟然老早就存在了,且已經成熟運用多年,深感慚愧啊,我到現在都還沒接觸過。
官方解釋
先是檢視了柯里化的介紹
在電腦科學中,柯里化(Currying)是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。
這句話聽起來有點拗口啊,反正我沒能完全理解,不過沒關係,我們可以檢視網上大牛們的解釋,經過一頓搜尋,算是屢清楚了頭緒。
我的理解
我們先來看個栗子,這是一個求和的函式,求4個引數的和,那麼我們在使用這個函式的時候,就要先知道每個引數的具體的值,然後才能呼叫這個函式。
function add(a, b, c, d) {
return a + b + c + d;
};
console.log(add(2,3,4,5)) // 14
複製程式碼
而假如,我們想讓呼叫更加靈活一些,比如改成下面這種呼叫方式
console.log(add(2)(3)(4)(5)) // 14
複製程式碼
可以看到,這種呼叫方式有個好處,不需要等待所有引數明確後在執行具體方法,當在引數不滿足的情況下,每次呼叫返回的是函式,比如 add(2)
返回的是一個函式,返回的函式不斷的接受新的引數,直到滿足預定好的4個引數時,便輸出結果。
而 add(2,3,4,5) => add(2)(3)(4)(5)
這一個過程便是函式柯里化的過程。這樣子解釋,不知道大夥好理解不?
talk is cheap , show me the code,ok 那我們看下具體的實現,先寫一個函式將 add()
轉換成柯里化函式
/**
* 轉換函式
* @param {*} fn 目標函式
* @param {...any} args 其他引數
*/
const createCurry = (fn, ...args) => {
// 獲取目標函式的引數個數
let length = fn.length;
return (...rest) => {
// 將已有的引數和新的引數合併
let allArgs = args.slice(0);
allArgs.push(...rest);
// 若引數個數已經滿足目標函式的引數要求,則執行目標函式。否則繼續返回轉換函式
if (allArgs.length < length) {
return createCurry.call(this, fn, ...allArgs)
} else {
return fn.apply(this, allArgs)
}
}
}
function add(a, b, c, d) {
return a + b + c + d;
};
複製程式碼
使用如下:
const curryAdd = createCurry(add,2);
const sum = curryAdd(3)(4)(5);
console.log(sum) // 14
複製程式碼
轉換柯里化函式有個關鍵的點,那就是要明確觸發條件,比如說上面的栗子中,我們的觸發條件是引數的個數,根據引數的個數來區分返回的是函式還是具體的結果。
柯里化函式的特點
通過上面的栗子我們可以看出柯里化函式有這麼幾個特點:
- 引數複用
- 業務解耦,呼叫時機靈活
- 延遲執行,部分求值
繼續舉栗子
光說理論太枯燥,那麼我們把上面的栗子在稍微擴充套件下,把add
函式改成不定引數,就是說,我可以傳n個引數,求這n個引數的和,呼叫如下
const curryAdd = createCurry(add);
const sum = curryAdd(3)(4)(5); // 12
const sum = curryAdd(3)(4)(5)(6); // 18
複製程式碼
OK,首先我們要對add
函式做下修改
/**
* 不定引數求和
* @param {...any} arg
*/
function add(...arg){
return arg.reduce((result,value)=>{ return result += value },0)
}
複製程式碼
那麼這個函式使用就變成 add(1,2,3)
,但我們的目標是這樣的 add(1)(2)(3)
,在寫轉換函式之前,我們先要確定我們的觸發條件,我想到的條件是:當新傳入的引數個數為 0 的時候,就是沒有引數,就執行目標函式,否則返回函式。那麼柯里化函式的呼叫方式需要稍微改成如下
add(1)(2)(3)();
複製程式碼
具體的轉換函式和呼叫如下
const createCurry = (fn, ...args) => {
return (...rest) => {
let allArgs = args.slice(0);
allArgs.push(...rest)
// 觸發條件
if (rest.length !== 0) {
return createCurry.call(this, fn, ...allArgs)
} else {
return fn.apply(this, allArgs)
}
}
}
const curryAdd = createCurry1(add);
console.log(curryAdd(1)(2)(3)(4)()) // 10
複製程式碼
柯里化到底是什麼?
通過上面兩個小栗子,大夥應該有個大致概念了,所以柯里化是一種設計思想,主動掌握了函式的控制權,根據我們的需要,設定相應的觸發機制。有句話,讓程式碼編寫程式碼,柯里化算是有那麼點意思吧,函式生成函式,函式去呼叫函式,我們只要編寫原子性的函式和設定好條件。
再來兩個栗子
陣列處理
寫個通用的陣列處理柯里化函式,currying
的第一個引數是目標函式,currying
直接返回函式,沒有特定的觸發條件,在使用mapSQ([1, 2, 3, 4, 5])
會將陣列和currying
的第二個引數一起傳給目標函式,即currying
第一個引數,詳細如下:
function currying(fn) {
var slice = Array.prototype.slice,
__args = slice.call(arguments, 1);
return function () {
var __inargs = slice.call(arguments);
return fn.apply(null, __args.concat(__inargs));
};
}
function square(i) {
return i * i;
}
function double(i) {
return i *= 2;
}
function map(handeler, list) {
return list.map((value) => {
return handeler(value)
});
}
var mapSQ = currying(map, square); // 平方
mapSQ([1, 2, 3, 4, 5]); //[1, 4, 9, 16, 25]
var mapTwo = currying(map, double); // 兩倍
mapSQ([1, 2, 3, 4, 5]); //[2,4,6,8,10]
複製程式碼
以引數長度為條件的轉換函式
這個栗子是在 30secondsofcode 上看到,我覺得寫得非常精簡直觀,給大家分享下
const curry = (fn, arity = fn.length, ...args) => arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);
curry(Math.pow)(2)(10); // 1024
curry(Math.min, 3)(10)(50)(2); // 2
複製程式碼
官方寫成了一行,閱讀可能不太方便,我改成通俗版,加了些註釋
/**
* 生成柯里化函式,以引數長度達標作為觸發條件
* @param {*} fn 目標函式
* @param {*} arity 目標函式引數個數
* @param {...any} args 呼叫傳入的引數
*/
const curry = (fn, arity = fn.length, ...args) => {
if (arity <= args.length) {
return fn(...args)
} else {
return curry.bind(null, fn, arity, ...args);
}
}
複製程式碼
小結
函式柯里化可以給我們帶來很多想象,可以將耦合的業務邏輯拆解,使得函式程式設計更加純粹。不過我個人覺得柯里化函式要是太複雜,對大大降低程式碼的可閱讀性和可維護性,所以柯里化雖然看著高大上,但還是不能濫用。