函數(shù)柯里化學(xué)習(xí)筆記

導(dǎo)讀:函數(shù)柯里化currying的概念最早由俄國數(shù)學(xué)家Moses Sch?nfinkel發(fā)明肚医,而后由著名的數(shù)理邏輯學(xué)家Haskell Curry將其豐富和發(fā)展觉既,currying由此得名抹剩。
定義:currying又稱部分求值。一個(gè)currying的函數(shù)首先會(huì)接受一些參數(shù),接受了這些參數(shù)之后驯嘱,該函數(shù)并不會(huì)立即求值假瞬,而是繼續(xù)返回另外一個(gè)函數(shù)陕靠,剛才傳入的參數(shù)在函數(shù)形成的閉包中被保存起來。待到函數(shù)被真正需要求值的時(shí)候脱茉,之前傳入的所有參數(shù)都會(huì)被一次性用于求值剪芥。
一個(gè)簡單curry的栗子

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

//函數(shù)只能傳一個(gè)參數(shù)時(shí)候?qū)崿F(xiàn)加法
function curry(a) {
    return function(b) {
        return a + b;
    }
}
var add2 = curry(2); //add2也就是第一個(gè)參數(shù)為2的add版本
console.log(add2(3))//5

通過以上簡單介紹我們大概了解了,函數(shù)柯里化基本是在做這么一件事情:只傳遞給函數(shù)一部分參數(shù)來調(diào)用它琴许,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)税肪。用公式表示就是我們要做的事情其實(shí)是

fn(a,b,c,d)=>fn(a)(b)(c)(d);

fn(a,b,c,d)=>fn(a,b)(c)(d)益兄;

fn(a,b,c,d)=>fn(a)(b锻梳,c,d)净捅;

......

再或者這樣:

fn(a,b,c,d)=>fn(a)(b)(c)(d)()疑枯;

fn(a,b,c,d)=>fn(a);fn(b)蛔六;fn(c)荆永;fn(d);fn()国章;

但不是這樣:

fn(a,b,c,d)=>fn(a)具钥;

fn(a,b,c,d)=>fn(a,b)捉腥;

......

這類不屬于柯里化內(nèi)容氓拼,它也有個(gè)專業(yè)的名字叫偏函數(shù),這個(gè)之后我們也會(huì)提到抵碟。
下面我們繼續(xù)把之前的add改為通用版本:

const curry = (fn, ...arg) => {
    let all = arg;
    return (...rest) => {
        all.push(...rest);
        return fn.apply(null, all);
    }
}
let add2 = curry(add, 2)
console.log(add2(8));    //10
add2 = curry(add);
console.log(add2(2,8)); //10

如果你想給函數(shù)執(zhí)行綁定執(zhí)行環(huán)境也很簡單桃漾,可以多傳入個(gè)參數(shù):

const curry = (fn, constext, ...arg) => {
    let all = arg;
    return (...rest) => {
        all.push(...rest);
        return fn.apply(constext, all);
    }
}

不過到目前我們并沒有實(shí)現(xiàn)柯里化,就是類似fn(a,b,c,d)=>fn(a)(b)(c)(d)拟逮,這樣的轉(zhuǎn)化撬统,原因也很明顯,我們curry之后的add2函數(shù)只能執(zhí)行一次敦迄,不能夠sdd2(5)(8)這樣執(zhí)行恋追,因?yàn)槲覀儧]有在函數(shù)第一次執(zhí)行完后返回一個(gè)函數(shù),而是返回的值罚屋,所以無法繼續(xù)調(diào)用苦囱。

所以我們繼續(xù)實(shí)現(xiàn)我們的curry函數(shù),要實(shí)現(xiàn)的點(diǎn)也明確了脾猛,柯里化后的函數(shù)在傳入?yún)?shù)未達(dá)到柯里化前的個(gè)數(shù)時(shí)候我們不能返回值撕彤,應(yīng)該返回函數(shù)讓它繼續(xù)執(zhí)行(如果你閱讀到這里可以試著自己實(shí)現(xiàn)一下),下面給出一種簡單的實(shí)現(xiàn)方式:

const curry = (fn, ...arg) => {
    let all = arg || [],
        length = fn.length;
    return (...rest) => {
        let _args = all.slice(0); //拷貝新的all猛拴,避免改動(dòng)公有的all屬性羹铅,導(dǎo)致多次調(diào)用_args.length出錯(cuò)
        _args.push(...rest);
        if (_args.length < length) {
            return curry.call(this, fn, ..._args);
        } else {
            return fn.apply(this, _args);
        }
    }
}
let add2 = curry(add, 2)
console.log(add2(8));//10
console.log(add2(8, 1));//10
console.log(add2(8)(1));//error
add2 = curry(add);
console.log(add2(2, 8));
console.log(add2(2)(8));
let test = curry(function(a, b, c) {
console.log(a + b + c);
})
test(1, 2, 3);
test(1, 2)(3);
test(1)(2)(3);

這里代碼邏輯其實(shí)很簡單,就是判斷參數(shù)是否已經(jīng)達(dá)到預(yù)期的值(函數(shù)柯里化之前的參數(shù)個(gè)數(shù))愉昆,如果沒有繼續(xù)返回函數(shù)职员,達(dá)到了就執(zhí)行函數(shù)然后返回值,唯一需要注意的點(diǎn)我在注釋里寫出來了all相當(dāng)于閉包引用的變量是公用的跛溉,需要在每個(gè)返回的函數(shù)里拷貝一份焊切;

好了到這里我們基本實(shí)現(xiàn)了柯里化函數(shù)扮授,我們來看文章開始羅列的公式,細(xì)心的同學(xué)應(yīng)該能發(fā)現(xiàn):

fn(a,b,c,d)=>fn(a)(b)(c)(d)()蛛蒙;//mod1

fn(a,b,c,d)=>fn(a)糙箍;fn(b);fn(c)牵祟;fn(d);fn()抖格;//mod2

這兩種我們的curry還未實(shí)現(xiàn)诺苹,對于這兩個(gè)公式其實(shí)是一樣的,寫法不同而已雹拄,對比之前的實(shí)現(xiàn)就是多了一個(gè)要素收奔,函數(shù)執(zhí)行返回值的觸發(fā)時(shí)機(jī)和被柯里化函數(shù)的參數(shù)的不確定性,好了我們來簡單修改一下代碼:

const curry = (fn, ...arg) => {
    let all = arg || [],
        length = fn.length;
    return (...rest) => {
        let _args = all;
        _args.push(...rest);
        if (rest.length === 0) {
       all=[];
            return fn.apply(this, _args);
        } else {
            return curry.call(this, fn, ..._args);
        }
    }
}
let test = curry(function(...rest) {
    let args = rest.map(val => val * 10);
    console.log(args);
})
test(2);
test(2);
test(3);
test();//[20, 20, 30]
test(5);
test();//[50]
test(2)(2)(2)(3)(4)(5)(6)();//
test(2, 3, 4, 5, 6, 7)();//

現(xiàn)在我們這個(gè)test函數(shù)的參數(shù)就可以任意傳滓玖,可多可少坪哄,至于在什么時(shí)候執(zhí)行返回值,控制權(quán)在我們(這里是設(shè)置的傳入?yún)?shù)為空時(shí)候觸發(fā)函數(shù)執(zhí)行返回值)势篡,當(dāng)然根據(jù)這邏輯我們能改造出來很多我們期望它按我們需求傳參翩肌、執(zhí)行的函數(shù)——這里我們就體會(huì)到了高階函數(shù)的靈活多變,讓使用者有更多發(fā)揮空間禁悠。

到這里我們科里化基本說完了念祭,下面我們順帶說一下偏函數(shù),如果你上邊柯里化的代碼都熟悉了碍侦,那么對于偏函數(shù)的這種轉(zhuǎn)化形式應(yīng)該得心應(yīng)手了:

fn(a,b,c,d)=>fn(a)粱坤;

fn(a,b,c,d)=>fn(a,b)瓷产;

我們還是先來看代碼吧

function part(fn, ...arg) {
    let all = arg || [];
    return (...rest) => {
        let args = all.slice(0);
        args.push(...rest);
        return fn.apply(this, args)
    }
}

function add(a = 0, b = 0, c = 0) {
    console.log(a + b + c);
}
let addPart = part(add);
addPart(9); //9
addPart(9, 11);//20

很簡單了站玄,我們現(xiàn)在的addPar就能隨便傳參都能調(diào)用了濒旦,當(dāng)然我們也能控制函數(shù)之調(diào)用某一個(gè)或者多個(gè)參數(shù)株旷,例如這樣:

        //偏han shu 
        function part(fn) {
            return (...arguments) => {
                return fn.call(this, arguments[0])
            }
        };      
        let newA = ['33','222','999','99888','2345'].map(part(parseInt));
        console.log('newA is ', newA)

我們想用parseInt幫我們轉(zhuǎn)化個(gè)數(shù)組疤估,但是我們沒法改動(dòng)parseInt的代碼,所以控制一下傳參就行了铃拇,這樣我們map就傳入的參數(shù)只取到第一個(gè)钞瀑,得到了我們的期望值。

Function.prototype.bind 方法也是柯里化應(yīng)用

與 call/apply 方法直接執(zhí)行不同雕什,bind 方法 將第一個(gè)參數(shù)設(shè)置為函數(shù)執(zhí)行的上下文,其他參數(shù)依次傳遞給調(diào)用方法(函數(shù)的主體本身不執(zhí)行贷岸,可以看成是延遲執(zhí)行)壹士,并動(dòng)態(tài)創(chuàng)建返回一個(gè)新的函數(shù), 這符合柯里化特點(diǎn)偿警。

var foo = {x: 888};
var bar = function () {
    console.log(this.x);
}.bind(foo);               // 綁定
bar();                     // 888

下面是一個(gè) bind 函數(shù)的模擬躏救,testBind 創(chuàng)建并返回新的函數(shù)螟蒸,在新的函數(shù)中將真正要執(zhí)行業(yè)務(wù)的函數(shù)綁定到實(shí)參傳入的上下文,延遲執(zhí)行了七嫌。

Function.prototype.testBind = function (scope) {//提前固定易變參數(shù)。
    var fn = this;                    // this 指向的是調(diào)用 testBind 方法的一個(gè)函數(shù)英妓, 
    return function () {
        return fn.apply(scope);
    }
};
var testBindBar = bar.testBind(foo);  // 綁定 foo绍赛,延遲執(zhí)行
console.log(testBindBar);             // Function (可見蔓纠,bind之后返回的是一個(gè)延遲執(zhí)行的新函數(shù))
testBindBar(); 

這里要注意 prototype 中 this 的理解惹资。

反柯里化

Array.prototype上的方法原本只能用來操作array對象。但用call和apply可以把任意對象當(dāng)作this傳入某個(gè)方法猴誊,這樣一來侮措,方法中用到this的地方就不再局限于原來規(guī)定的對象,而是加以泛化并得到更廣的適用性

有沒有辦法把泛化this的過程提取出來呢分扎?反柯里化(uncurrying)就是用來解決這個(gè)問題的。反柯里化主要用于擴(kuò)大適用范圍墨状,創(chuàng)建一個(gè)應(yīng)用范圍更廣的函數(shù)菲饼。使本來只有特定對象才適用的方法,擴(kuò)展到更多的對象宏悦。

uncurrying的話題來自JavaScript之父Brendan Eich在2011年發(fā)表的一篇文章包吝。以下代碼是 uncurrying 的實(shí)現(xiàn)方式之一:

Function.prototype.uncurrying = function () { 
  var _this = this;
  return function() {
    var obj = Array.prototype.shift.call( arguments );
    return _this.apply( obj, arguments );
  };
};

另一種實(shí)現(xiàn)方法如下

Function.prototype.currying = function() {
    var _this = this;
    return function() {
        return Function.prototype.call.apply(_this, arguments);
    }
}

最終是都把this.method轉(zhuǎn)化成method(this,arg1,arg2....)以實(shí)現(xiàn)方法借用和this的泛化

下面是一個(gè)讓普通對象具備push方法的例子

 var push = Array.prototype.push.uncurrying(),
    obj = {};
  push(obj, 'first', 'two');
  console.log(obj);
/*obj {
    0 : "first",
    1 : "two"
}*/

通過uncurrying的方式源葫,Array.prototype.push.call變成了一個(gè)通用的push函數(shù)。這樣一來嚷狞,push函數(shù)的作用就跟Array.prototype.push一樣了储矩,同樣不僅僅局限于只能操作array對象。而對于使用者而言持隧,調(diào)用push函數(shù)的方式也顯得更加簡潔和意圖明了

最后逃片,再看一個(gè)例子

var toUpperCase = String.prototype.toUpperCase.uncurrying();
console.log(toUpperCase('avd')); // AVD
function AryUpper(ary) {
    return ary.map(toUpperCase);
}
console.log(AryUpper(['a', 'b', 'c'])); // ["A", "B", "C"]
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市呀狼,隨后出現(xiàn)的幾起案子损离,更是在濱河造成了極大的恐慌哥艇,老刑警劉巖僻澎,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件貌踏,死亡現(xiàn)場離奇詭異,居然都是意外死亡窟勃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門眷昆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汁咏,“玉大人,你說我怎么就攤上這事帅刊『洳担” “怎么了弟灼?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵冒黑,是天一觀的道長。 經(jīng)常有香客問我掩驱,道長冬竟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任涮帘,我火速辦了婚禮笑诅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吆你。我一直安慰自己,他們只是感情好妇多,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著者祖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪桃序。 梳的紋絲不亂的頭發(fā)上烂瘫,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音芦鳍,去河邊找鬼葛账。 笑死,一個(gè)胖子當(dāng)著我的面吹牛籍琳,可吹牛的內(nèi)容都是我干的贷祈。 我是一名探鬼主播喝峦,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼谣蠢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了眉踱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對情侶失蹤册烈,失蹤者是張志新(化名)和其女友劉穎婿禽,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谈宛,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吆录,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年琼牧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撬槽。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡趾撵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出暂题,到底是詐尸還是另有隱情,我是刑警寧澤薪者,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布剿涮,位于F島的核電站攻人,受9級(jí)特大地震影響悬槽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜陷谱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一烟逊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宪躯,春花似錦、人聲如沸详瑞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽精置。三九已至,卻和暖如春番宁,著一層夾襖步出監(jiān)牢的瞬間赖阻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國打工棋电, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人布隔。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像招刨,于是被迫代替她去往敵國和親哀军。 傳聞我的和親對象是個(gè)殘疾皇子沉眶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容