Js中Currying的應用
柯里化Currying
是把接受多個引數的函式變換成接受一個單一引數的函式,並且返回接受餘下的引數且返回結果的新函式的技術,是函數語言程式設計應用。
描述
如果說函數語言程式設計中有兩種操作是必不可少的那無疑就是柯里化Currying
和函式組合Compose
,柯里化其實就是流水線上的加工站,函式組合就是我們的流水線,它由多個加工站組成。對於加工站即柯里化Currying
,簡單來說就是將一個多元函式,轉換成一個依次呼叫的單元函式,也就是把一個多引數的函式轉化為單引數函式的方法,函式的柯里化是用於將一個操作分成多步進行,並且可以改變函式的行為,在我的理解中柯里化實際就是實現了一個狀態機,當達到指定引數時就從繼續接收引數的狀態轉換到執行函式的狀態。
簡單來說,通過柯里化可以把函式呼叫的形式改變。
f(a,b,c) → f(a)(b)(c)
與柯里化非常相似的概念有部分函式應用Partial Function Application
,這兩者不是相同的,部分函式應用強調的是固定一定的引數,返回一個更小元的函式。
// 柯里化
f(a,b,c) → f(a)(b)(c)
// 部分函式呼叫
f(a,b,c) → f(a)(b,c) / f(a,b)(c)
柯里化強調的是生成單元函式,部分函式應用的強調的固定任意元引數,而我們平時生活中常用的其實是部分函式應用,這樣的好處是可以固定引數,降低函式通用性,提高函式的適合用性,在很多庫函式中curry
函式都做了很多優化,已經不是純粹的柯里化函式了,可以將其稱作高階柯里化,這些版本實現可以根據你輸入的引數個數,返回一個柯里化函式/結果值,即如果你給的引數個數滿足了函式條件,則返回值。
實現
實現一個簡單的柯里化的函式,可以通過閉包來實現。
var add = function(x) {
return function(y) {
return x + y;
};
};
console.log(add(1)(2)); // 3
當有多個引數時,這樣顯然不夠優雅,於是封裝一個將普通函式轉變為柯里化函式的函式。
function convertToCurry(funct, ...args) {
const argsLength = funct.length;
return function(..._args) {
_args.unshift(...args);
if (_args.length < argsLength) return convertToCurry.call(this, funct, ..._args);
return funct.apply(this, _args);
}
}
var funct = (x, y, z) => x + y + z;
var addCurry = convertToCurry(funct);
var result = addCurry(1)(2)(3);
console.log(result); // 6
舉一個需要正則匹配驗證手機與郵箱的例子來展示柯里化的應用。
function convertToCurry(funct, ...args) {
const argsLength = funct.length;
return function(..._args) {
_args.unshift(...args);
if (_args.length < argsLength) return convertToCurry.call(this, funct, ..._args);
return funct.apply(this, _args);
}
}
var check = (regex, str) => regex.test(str);
var checkPhone = convertToCurry(check, /^1[34578]\d{9}$/);
var checkEmail = convertToCurry(check, /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
console.log(checkPhone("13300000000")); // true
console.log(checkPhone("13311111111")); // true
console.log(checkPhone("13322222222")); // true
console.log(checkEmail("13300000000@a.com")); // true
console.log(checkEmail("13311111111@a.com")); // true
console.log(checkEmail("13322222222@a.com")); // true
應用
高階柯里化有一個應用方面在於Thunk
函式,Thunk
函式是應用於編譯器的傳名呼叫實現,往往是將引數放到一個臨時函式之中,再將這個臨時函式傳入函式體,這個臨時函式就叫做Thunk
函式。Thunk
函式將引數替換成單引數的版本,且只接受回撥函式作為引數。
// 假設一個延時函式需要傳遞一些引數
// 通常使用的版本如下
var delayAsync = function(time, callback, ...args){
setTimeout(() => callback(...args), time);
}
var callback = function(x, y, z){
console.log(x, y, z);
}
delayAsync(1000, callback, 1, 2, 3);
// 使用Thunk函式
var thunk = function(time, ...args){
return function(callback){
setTimeout(() => callback(...args), time);
}
}
var callback = function(x, y, z){
console.log(x, y, z);
}
var delayAsyncThunk = thunk(1000, 1, 2, 3);
delayAsyncThunk(callback);
實現一個簡單的Thunk
函式轉換器,對於任何函式,只要引數有回撥函式,就能寫成Thunk
函式的形式。
var convertToThunk = function(funct){
return function (...args){
return function (callback){
return funct.apply(this, args);
}
};
};
var callback = function(x, y, z){
console.log(x, y, z);
}
var delayAsyncThunk = convertToThunk(function(time, ...args){
setTimeout(() => callback(...args), time);
});
thunkFunct = delayAsyncThunk(1000, 1, 2, 3);
thunkFunct(callback);
Thunk
函式在ES6
之前可能應用比較少,但是在ES6
之後,出現了Generator
函式,通過使用Thunk
函式就可以可以用於Generator
函式的自動流程管理。首先是關於Generator
函式的基本使用,呼叫一個生成器函式並不會馬上執行它裡面的語句,而是返回一個這個生成器的迭代器iterator
物件,他是一個指向內部狀態物件的指標。當這個迭代器的next()
方法被首次(後續)呼叫時,其內的語句會執行到第一個(後續)出現yield
的位置為止,yield
後緊跟迭代器要返回的值,也就是指標就會從函式頭部或者上一次停下來的地方開始執行到下一個yield
。或者如果用的是yield*
,則表示將執行權移交給另一個生成器函式(當前生成器暫停執行)。
function* f(x) {
yield x + 10;
yield x + 20;
return x + 30;
}
var g = f(1);
console.log(g); // f {<suspended>}
console.log(g.next()); // {value: 11, done: false}
console.log(g.next()); // {value: 21, done: false}
console.log(g.next()); // {value: 31, done: true}
console.log(g.next()); // {value: undefined, done: true} // 可以無限next(),但是value總為undefined,done總為true
由於Generator
函式能夠將函式的執行暫時掛起,那麼他就完全可以操作一個非同步任務,當上一個任務完成之後再繼續下一個任務,下面這個例子就是將一個非同步任務同步化表達,當上一個延時定時器完成之後才會進行下一個定時器任務,可以通過這種方式解決一個非同步巢狀的問題,例如利用回撥的方式需要在一個網路請求之後加入一次回撥進行下一次請求,很容易造成回撥地獄,而通過Generator
函式就可以解決這個問題,事實上async/await
就是利用的Generator
函式以及Promise
實現的非同步解決方案。
var it = null;
function f(){
var rand = Math.random() * 2;
setTimeout(function(){
if(it) it.next(rand);
},1000)
}
function* g(){
var r1 = yield f();
console.log(r1);
var r2 = yield f();
console.log(r2);
var r3 = yield f();
console.log(r3);
}
it = g();
it.next();
雖然上邊的例子能夠自動執行,但是不夠方便,現在實現一個Thunk
函式的自動流程管理,其自動幫我們進行回撥函式的處理,只需要在Thunk
函式中傳遞一些函式執行所需要的引數比如例子中的index
,然後就可以編寫Generator
函式的函式體,通過左邊的變數接收Thunk
函式中funct
執行的引數,在使用Thunk
函式進行自動流程管理時,必須保證yield
後是一個Thunk
函式。
關於自動流程管理run
函式,首先需要知道在呼叫next()
方法時,如果傳入了引數,那麼這個引數會傳給上一條執行的yield
語句左邊的變數,在這個函式中,第一次執行next
時並未傳遞引數,而且在第一個yield
上邊也並不存在接收變數的語句,無需傳遞引數,接下來就是判斷是否執行完這個生成器函式,在這裡並沒有執行完,那麼將自定義的next
函式傳入res.value
中,這裡需要注意res.value
是一個函式,可以在下邊的例子中將註釋的那一行執行,然後就可以看到這個值是f(funct){...}
,此時我們將自定義的next
函式傳遞後,就將next
的執行許可權交予了f
這個函式,在這個函式執行完非同步任務後,會執行回撥函式,在這個回撥函式中會觸發生成器的下一個next
方法,並且這個next
方法是傳遞了引數的,上文提到傳入引數後會將其傳遞給上一條執行的yield
語句左邊的變數,那麼在這一次執行中會將這個引數值傳遞給r1
,然後在繼續執行next
,不斷往復,直到生成器函式結束執行,這樣就實現了流程的自動管理。
function thunkFunct(index){
return function f(funct){
var rand = Math.random() * 2;
setTimeout(() => funct({rand:rand, index: index}), 1000)
}
}
function* g(){
var r1 = yield thunkFunct(1);
console.log(r1.index, r1.rand);
var r2 = yield thunkFunct(2);
console.log(r2.index, r2.rand);
var r3 = yield thunkFunct(3);
console.log(r3.index, r3.rand);
}
function run(generator){
var g = generator();
var next = function(data){
var res = g.next(data);
if(res.done) return ;
// console.log(res.value);
res.value(next);
}
next();
}
run(g);
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://www.jianshu.com/p/5e1899fe7d6b
https://zhuanlan.zhihu.com/p/108594470
https://juejin.im/post/6844903936378273799#heading-12
https://blog.csdn.net/crazypokerk_/article/details/97674338
http://www.qiutianaimeili.com/html/page/2019/05/54g0vvxycyg.html
https://baike.baidu.com/item/%E6%9F%AF%E9%87%8C%E5%8C%96/10350525?fr=aladdin
https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch4.html#%E4%B8%8D%E4%BB%85%E4%BB%85%E6%98%AF%E5%8F%8C%E5%85%B3%E8%AF%AD%E5%92%96%E5%96%B1