摘要:函數柯里化的對偶是,一種使用匿名單參數函數來實現多參數函數的方法。這是基于的富應用開發的方法實現反柯里化可能遇到這種情況拿到一個柯里化后的函數,卻想要它柯里化之前的版本,這本質上就是想將類似的函數變回類似的函數。
什么是柯里化? 官方的說法
在計算機科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術。這個技術由克里斯托弗·斯特雷奇以邏輯學家哈斯凱爾·加里命名的,盡管它是Moses Sch?nfinkel和戈特洛布·弗雷格發明的。
在直覺上,柯里化聲稱如果你固定某些參數,你將得到接受余下參數的一個函數。
在理論計算機科學中,柯里化提供了在簡單的理論模型中,比如:只接受一個單一參數的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) { // .. }
有這樣的一個場景:我們需要對多個不同的接口發起HTTP請求,有下列兩種做法:
在調用ajax()函數時,傳入全局URL常量。
創建一個已經預設URL實參的函數引用。
下面我們創建一個新函數,其內部仍然發起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數組收集了后面傳入的實參,保存起來稍后使用。
我們創建并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的優勢所在了,沒錯,柯里化就是:將一個帶有多個參數的函數轉換為一次一個的函數的過程。每次調用函數時,它只接受一個參數,并返回一個函數,直到傳遞所有參數為止。
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.
假設我們已經創建了一個柯里化版本的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
上述函數是我之前的JavaScript設計模式與開發實踐讀書筆記之閉包與高階函數所寫的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; // 返回這個函數的引用 } } }
使用實例:
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屬性,表明函數期望接受的參數個數。因此我們可以充分利用預傳參數的這個特點。
借鑒自mqyqingfeng:
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.length和fn.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方法兼容構造函數。
Function.prototype.bind() by MDN如下說到:
綁定函數適用于用new操作符 new 去構造一個由目標函數創建的新的實例。當一個綁定函數是用來構建一個值的,原來提供的 this 就會被忽略。然而, 原先提供的那些參數仍然會被前置到構造函數調用的前面。
這是基于MVC的JavaScript Web富應用開發的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() {}, 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柯里化或偏函數有什么用?
無論是柯里化還是偏應用,我們都能進行部分傳值,而傳統函數調用則需要預先確定所有實參。如果你在代碼某一處只獲取了部分實參,然后在另一處確定另一部分實參,這個時候柯里化和偏應用就能派上用場。
另一個最能體現柯里化應用的的是,當函數只有一個形參時,我們能夠比較容易地組合它們(單一職責原則(Single responsibility principle))。因此,如果一個函數最終需要三個實參,那么它被柯里化以后會變成需要三次調用,每次調用需要一個實參的函數。當我們組合函數時,這種單元函數的形式會讓我們處理起來更簡單。
歸納下來,主要為以下常見的三個用途:
延遲計算
參數復用
動態生成函數
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/89977.html
摘要:原文鏈接和都支持函數的柯里化函數的柯里化還與的函數編程有很大的聯系如果你感興趣的話可以在這些方面多下功夫了解相信收獲一定很多看本篇文章需要知道的一些知識點函數部分的閉包高階函數不完全函數文章后面有對這些知識的簡單解釋大家可以看看什么是柯里化 原文鏈接 Haskell和scala都支持函數的柯里化,JavaScript函數的柯里化還與JavaScript的函數編程有很大的聯系,如果你感興...
摘要:作為函數式編程語言,帶來了很多語言上的有趣特性,比如柯里化和反柯里化。在一些函數式編程語言中,會定義一個特殊的占位變量。個人理解不知道對不對延遲執行柯里化的另一個應用場景是延遲執行。不斷的柯里化,累積傳入的參數,最后執行。作為函數式編程語言,JS帶來了很多語言上的有趣特性,比如柯里化和反柯里化。 這里可以對照另外一篇介紹 JS 反柯里化 的文章一起看~ 1. 簡介 柯里化(Currying)...
摘要:作為函數式編程語言,帶來了很多語言上的有趣特性,比如柯里化和反柯里化。而反柯里化,從字面講,意義和用法跟函數柯里化相比正好相反,擴大適用范圍,創建一個應用范圍更廣的函數。作為函數式編程語言,JS帶來了很多語言上的有趣特性,比如柯里化和反柯里化。 可以對照另外一篇介紹 JS 柯里化 的文章一起看~ 1. 簡介 柯里化,是固定部分參數,返回一個接受剩余參數的函數,也稱為部分計算函數,目的是為了縮...
摘要:組合的概念是非常直觀的,并不是函數式編程獨有的,在我們生活中或者前端開發中處處可見。其實我們函數式編程里面的組合也是類似,函數組合就是一種將已被分解的簡單任務組織成復雜的整體過程。在函數式編程的世界中,有這樣一種很流行的編程風格。 JavaScript函數式編程,真香之認識函數式編程(一) 該系列文章不是針對前端新手,需要有一定的編程經驗,而且了解 JavaScript 里面作用域,閉...
摘要:如果你對函數式編程有一定了解,函數柯里化是不可或缺的,利用函數柯里化,可以在開發中非常優雅的處理復雜邏輯。同樣先看簡單版本的方法,以方法為例,代碼來自高級程序設計加強版實現上面函數,可以換成任何其他函數,經過函數處理,都可以轉成柯里化函數。 我們經常說在Javascript語言中,函數是一等公民,它們本質上是十分簡單和過程化的。可以利用函數,進行一些簡單的數據處理,return 結果,...
摘要:三使用場景場景性能優化可以將一些模板代碼通過柯里化的形式預先定義好,例如這段代碼的作用就是根據瀏覽器的類型決定事件添加的方式。場景擴展能力中的方法,就是通過柯里化實現的四總結通過本文的介紹,相信你對柯里化已經有一個全新的認識了。 歡迎關注我的公眾號睿Talk,獲取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 柯里化...
閱讀 2019·2021-11-24 09:39
閱讀 1882·2019-08-30 15:55
閱讀 2175·2019-08-30 15:53
閱讀 572·2019-08-29 13:16
閱讀 990·2019-08-26 12:20
閱讀 2387·2019-08-26 11:58
閱讀 3151·2019-08-26 10:19
閱讀 3310·2019-08-23 18:31