JS柯里化

什么是柯里化术幔?

官方的說法

在計算機科學中,柯里化(英語:Currying北苟,又譯為卡瑞化加里化闪朱,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術捐川。這個技術由克里斯托弗·斯特雷奇以邏輯學家哈斯凱爾·加里命名的脓鹃,盡管它是Moses Sch?nfinkel戈特洛布·弗雷格發(fā)明的

  • 在直覺上古沥,柯里化聲稱如果你固定某些參數瘸右,你將得到接受余下參數的一個函數
  • 在理論計算機科學中岩齿,柯里化提供了在簡單的理論模型中太颤,比如:只接受一個單一參數的lambda演算中,研究帶有多個參數的函數的方式盹沈。
  • 函數柯里化的對偶是Uncurrying龄章,一種使用匿名單參數函數來實現多參數函數的方法。

方便的理解

Currying概念其實很簡單,只傳遞給函數一部分參數來調用它瓦堵,讓它返回一個函數去處理剩下的參數基协。

如果我們需要實現一個求三個數之和的函數:

function add(x, y, z) {
  return x + y + z;
}
console.log(add(1, 2, 3)); // 6
var add = function(x) {
  return function(y) {
    return function(z) {
      return x + y + z;
    }
  }
}

var addOne = add(1);
var addOneAndTwo = addOne(2);
var addOneAndTwoAndThree = addOneAndTwo(3);

console.log(addOneAndTwoAndThree);
  • 這里我們定義了一個add函數,它接受一個參數并返回一個新的函數菇用。調用add之后澜驮,返回的函數就通過閉包的方式記住了add的第一個參數。一次性地調用它實在是有點繁瑣惋鸥,好在我們可以使用一個特殊的curry幫助函數(helper function)使這類函數的定義和調用更加容易杂穷。

ES6的箭頭函數,我們可以將上面的add實現成這樣:

const add = x => y => z => x + y + z;

好像使用箭頭函數更清晰了許多卦绣。

偏函數耐量?

來看這個函數:

function ajax(url, data, callback) {
  // ..
}

有這樣的一個場景:我們需要對多個不同的接口發(fā)起HTTP請求,有下列兩種做法:

  • 在調用ajax()函數時滤港,傳入全局URL常量廊蜒。
  • 創(chuàng)建一個已經預設URL實參的函數引用。

下面我們創(chuàng)建一個新函數溅漾,其內部仍然發(fā)起ajax()請求山叮,此外在等待接收另外兩個實參的同時,我們手動將ajax()第一個實參設置成你關心的API地址添履。

對于第一種做法屁倔,我們可能產生如下調用方式:

function ajaxTest1(data, callback) {
  ajax('http://www.test.com/test1', data, callback);
}

function ajaxTest2(data, callback) {
  ajax('http://www.test.com/test2', data, callback);
}

對于這兩個類似的函數,我們還可以提取出如下的模式:

function beginTest(callback) {
  ajaxTest1({
    data: GLOBAL_TEST_1,
  }, callback);
}
  • 相信您已經看到了這樣的模式:我們在函數調用現場(function call-site)暮胧,將實參應用(apply) 于形參锐借。如你所見,我們一開始僅應用了部分實參 —— 具體是將實參應用到URL形參 —— 剩下的實參稍后再應用往衷。

上述概念即為偏函數的定義钞翔,偏函數一個減少函數參數個數的過程;這里的參數個數指的是希望傳入的形參的數量炼绘。我們通過ajaxTest1()把原函數ajax()的參數個數從3個減少到了2個嗅战。

我們這樣定義一個partial()函數:

function partial(fn, ...presetArgs) {
  return function partiallyApplied(...laterArgs) {
    return fn(...presetArgs, ...laterArgs);
  }
}
  • partial()函數接收fn參數,來表示被我們偏應用實參(partially apply)的函數俺亮。接著驮捍,fn形參之后,presetArgs數組收集了后面?zhèn)魅氲膶崊⒔旁4嫫饋砩院笫褂谩?/p>

  • 我們創(chuàng)建并return了一個新的內部函數(為了清晰明了东且,我們把它命名為partiallyApplied(..)),該函數中本讥,laterArgs數組收集了全部實參珊泳。

使用箭頭函數鲁冯,則更為簡潔:

var partial =  (fn, ...presetArgs) =>(...laterArgs) =>fn(...presetArgs, ...laterArgs);

使用偏函數的這種模式,我們重構之前的代碼:

function ajax(url, data, callback) {
  // ..
}

var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');

再次思考beginTest()函數色查,我們使用partial()來重構它應該怎么做呢薯演?

function ajax(url, data, callback) {
  // ..
}

// 版本1
var beginTest = partial(ajax, 'http://www.test.com/test1', {
  data: GLOBAL_TEST_1,
});

// 版本2
var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');
var beginTest = partial(ajaxTest1, {
  data: GLOBAL_TEST_1,
});

一次傳一個

相信你已經在上述例子中看到了版本2比起版本1的優(yōu)勢所在了,沒錯秧了,柯里化就是:將一個帶有多個參數的函數轉換為一次一個的函數的過程跨扮。每次調用函數時,它只接受一個參數验毡,并返回一個函數衡创,直到傳遞所有參數為止。

The process of converting a function that takes multiple arguments into a function that takes them one at a time.Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.

假設我們已經創(chuàng)建了一個柯里化版本的ajax()函數curriedAjax()

curriedAjax('http://www.test.com/test1')({
  data: GLOBAL_TEST_1,
})
(function callback(data) {
  // dosomething
});

我們將三次調用分別拆解開來晶通,這也許有助于我們理解整個過程:

var ajaxTest1 = curriedAjax('http://www.test.com/test1');

var beginTest = ajaxTest1({
  data: GLOBAL_TEST_1,
});

var ajaxCallback = beginTest(function callback(data) {
  // dosomething
});

實現柯里化

那么璃氢,我們如何來實現一個自動的柯里化的函數呢?

var currying = function(fn) {
  var args = [];

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 沒傳參數時狮辽,調用這個函數
    } else {
      [].push.apply(args, arguments); // 傳入了參數一也,把參數保存下來
      return arguments.callee; // 返回這個函數的引用
    }
  }
}

調用上述currying()函數:

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var cost = currying(cost);

cost(100); // 傳入了參數,不真正求值
cost(200); // 傳入了參數喉脖,不真正求值
cost(300); // 傳入了參數塘秦,不真正求值

console.log(cost()); // 求值并且輸出600

我們在使用柯里化時,要注意同時為函數預傳的參數的情況动看。

因此把上述柯里化函數更改如下:

var currying = function(fn) {
  var args = Array.prototype.slice.call(arguments, 1);
  
  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 沒傳參數時,調用這個函數
    } else {
      [].push.apply(args, arguments); // 傳入了參數爪幻,把參數保存下來
      return arguments.callee; // 返回這個函數的引用
    }
  }
}

使用實例:

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var cost = currying(cost, 100);
cost(200); // 傳入了參數菱皆,不真正求值
cost(300); // 傳入了參數,不真正求值

console.log(cost()); // 求值并且輸出600

你可能會覺得每次都要在最后調用一下不帶參數的cost()函數比較麻煩挨稿,并且在cost()函數都要使用arguments參數不符合你的預期仇轻。我們知道函數都有一個length屬性,表明函數期望接受的參數個數奶甘。因此我們可以充分利用預傳參數的這個特點篷店。

function sub_curry(fn) {
  var args = [].slice.call(arguments, 1);
  return function() {
    return fn.apply(this, args.concat([].slice.call(arguments)));
  };
}

function curry(fn, length) {
  length = length || fn.length;
  var slice = Array.prototype.slice;
  return function() {
    if (arguments.length < length) {
      var combined = [fn].concat(slice.call(arguments));
      return curry(sub_curry.apply(this, combined), length - arguments.length);
    } else {
      return fn.apply(this, arguments);
    }
  };
}

在上述函數中,我們在currying的返回函數中臭家,每次把arguments.lengthfn.length作比較疲陕,一旦arguments.length達到了fn.length的數量,我們就去調用fn(return fn.apply(this, arguments);)

驗證:

var fn = curry(function(a, b, c) {
  return [a, b, c];
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

bind方法的實現

使用柯里化钉赁,能夠很方便地借用call()或者apply()實現bind()方法的polyfill蹄殃。

Function.prototype.bind = Function.prototype.bind || function(context) {
  var me = this;
  var args = Array.prototype.slice.call(arguments, 1);
  return function() {
    var innerArgs = Array.prototype.slice.call(arguments);
    var finalArgs = args.concat(innerArgs);
    return me.apply(contenxt, finalArgs);
  }
}
  • 上述函數有的問題在于不能兼容構造函數。我們通過判斷this指向的對象的原型屬性你踩,來判斷這個函數是否通過new作為構造函數調用诅岩,來使得上述bind方法兼容構造函數讳苦。

  • 綁定函數適用于用new操作符 new 去構造一個由目標函數創(chuàng)建的新的實例。當一個綁定函數是用來構建一個值的吩谦,原來提供的this 就會被忽略鸳谜。然而, 原先提供的那些參數仍然會被前置到構造函數調用的前面。

這是基于MVCJavaScript Web富應用開發(fā)的bind()方法實現:

Function.prototype.bind = function(oThis) {
  if (typeof this !== "function") {
    throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  }

  var aArgs = Array.prototype.slice.call(arguments, 1),
      fToBind = this,
      fNOP = function() {},
  var fBound = function() {
        return fToBind.apply(
          this instanceof fNOP && oThis ? this : oThis || window,aArgs.concat(Array.prototype.slice.call(arguments))
        );
  };
  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
  return fBound;
};

反柯里化(uncurrying)

可能遇到這種情況:拿到一個柯里化后的函數式廷,卻想要它柯里化之前的版本咐扭,這本質上就是想將類似f(1)(2)(3)的函數變回類似g(1,2,3)的函數。

下面是簡單的uncurrying的實現方式:

function uncurrying(fn) {
  return function(...args) {
    var ret = fn;
    for (let i = 0; i < args.length; i++) {
      ret = ret(args[i]); // 反復調用currying版本的函數
    }
    return ret; // 返回結果
  };
}

注意懒棉,不要以為uncurrying后的函數和currying之前的函數一模一樣草描,它們只是行為類似!

var currying = function(fn) {
  var args = Array.prototype.slice.call(arguments, 1);

  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args); // 沒傳參數時策严,調用這個函數
    } else {
      [].push.apply(args, arguments); // 傳入了參數穗慕,把參數保存下來
      return arguments.callee; // 返回這個函數的引用
    }
  }
}

function uncurrying(fn) {
  return function(...args) {
    var ret = fn;

    for (let i = 0; i < args.length; i++) {
      ret = ret(args[i]); // 反復調用currying版本的函數
    }
    return ret; // 返回結果
  };
}

var cost = (function() {
  var money = 0;
  return function() {
    for (var i = 0; i < arguments.length; i++) {
      money += arguments[i];
    }
    return money;
  }
})();

var curryingCost = currying(cost);
var uncurryingCost = uncurrying(curryingCost);
console.log(uncurryingCost(100, 200, 300)()); // 600

柯里化或偏函數有什么用?

無論是柯里化還是偏應用妻导,我們都能進行部分傳值逛绵,而傳統(tǒng)函數調用則需要預先確定所有實參。如果你在代碼某一處只獲取了部分實參倔韭,然后在另一處確定另一部分實參术浪,這個時候柯里化和偏應用就能派上用場。

另一個最能體現柯里化應用的的是寿酌,當函數只有一個形參時胰苏,我們能夠比較容易地組合它們(單一職責原則(Single responsibility principle))。因此醇疼,如果一個函數最終需要三個實參硕并,那么它被柯里化以后會變成需要三次調用,每次調用需要一個實參的函數秧荆。當我們組合函數時倔毙,這種單元函數的形式會讓我們處理起來更簡單。

歸納下來乙濒,主要為以下常見的三個用途:

  • 延遲計算
  • 參數復用
  • 動態(tài)生成函數
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末陕赃,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子颁股,更是在濱河造成了極大的恐慌么库,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甘有,死亡現場離奇詭異廊散,居然都是意外死亡,警方通過查閱死者的電腦和手機梧疲,發(fā)現死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門允睹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來运准,“玉大人,你說我怎么就攤上這事缭受⌒舶模” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵米者,是天一觀的道長韭畸。 經常有香客問我,道長蔓搞,這世上最難降的妖魔是什么胰丁? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮喂分,結果婚禮上锦庸,老公的妹妹穿的比我還像新娘。我一直安慰自己蒲祈,他們只是感情好甘萧,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著梆掸,像睡著了一般扬卷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酸钦,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天怪得,我揣著相機與錄音,去河邊找鬼卑硫。 笑死汇恤,一個胖子當著我的面吹牛,可吹牛的內容都是我干的拔恰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼基括,長吁一口氣:“原來是場噩夢啊……” “哼颜懊!你這毒婦竟也來了?” 一聲冷哼從身側響起风皿,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤河爹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后桐款,有當地人在樹林里發(fā)現了一具尸體咸这,經...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年魔眨,在試婚紗的時候發(fā)現自己被綠了媳维。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酿雪。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖侄刽,靈堂內的尸體忽然破棺而出指黎,到底是詐尸還是另有隱情,我是刑警寧澤州丹,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布醋安,位于F島的核電站,受9級特大地震影響墓毒,放射性物質發(fā)生泄漏吓揪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一所计、第九天 我趴在偏房一處隱蔽的房頂上張望柠辞。 院中可真熱鬧,春花似錦醉箕、人聲如沸钾腺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽放棒。三九已至,卻和暖如春己英,著一層夾襖步出監(jiān)牢的瞬間间螟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工损肛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留厢破,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓治拿,卻偏偏與公主長得像摩泪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子劫谅,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內容