柯里化在工程中有什麼好處?

剛看了一遍這個

第 4 章: 柯里化(curry)

感覺柯里化就是相當於把具有兩個入參的函數拆成一個外部單入參函數調用內部單入參函數,然後內部函數里有一個寫死的局部變數;

比如下邊這個,

var add = function(x) {
return function(y) {
return x + y;
};
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

我所能想到的, 就是如果我需要一堆addOne, addTwo, addThree...這種東西的時候, 柯里化會比較方便, 其他的好處, 真是想不出來;

請指導的大神指點, 謝謝!


從純粹的語義上,curry化就是lambda表達式的一個糖,不要想太多。


1. 因為js不存在函數級別的靜態變數(c語言中的概念),無法在函數重入的時候保存之前的狀態。如果你希望做一個可重入的方法(可以設置個初始值,之後函數返回的結果是基於這個初始值變化的),正如題主的例子。

2. curry可以返回一個新的函數,你可以將函數的引用賦值給新變數,也可以用它覆蓋原來的函數引用,如果是後者,你就可以通過在新函數中調用之前函數和before以及after的回調方法實現AOP,如果你看過redux的源碼你會發現它的中間件就是通過這種方式實現的。

3. js是支持可變參數的,我們開發中實現的通用方法會根據函數參數的類型或者數量而產生不一樣的行為,但有時會產生問題。例如["11","11","11","11"].map(parseInt)得到的結果會跟你預期的不一樣,就是因為parseInt支持可變參數,通過curry可以限定參數的個數,例如function curry(func){return function(argument){return func(argument)}},curry(parseInt)這樣包裝一下就只能保證只會傳入map方法回調的第一個參數。

暫時想到這麼多~


柯里化就是為了部分應用,當然AddTen這種明確寫一個函數意義不是很大,這只是個演示,讓你明白它的工作原理。實際上柯里化的部分應用常見於匿名函數中, 函數式思維就是要盡量減少代碼中的中間變數:

不是先寫一個addTen ,再

.map(addTen)

也不是:

.map(x =&> add(10, x))

只需

.map(add(10))

是不是很簡單實惠?

在F#中則連括弧都省略了:.map(add 10)

私貨:個人認為,一個函數式語言好不好,重要的不是支持了多少高級特性,而是讓你寫函數式時候是否方便,讀起來是否簡潔。至於那些在多範式裡面是否應該使用函數式語法,這個爭議很多。我覺得想用就用唄,但是最好用函數式語言。因為函數式語言的柯里化往往是自動的,當你沒有部分應用的時候,它就是個普通函數,只有使用了部分應用之後,才會生成柯里化結果。

以上討論是基於以下的上下文:

柯里化你茲不茲詞

| 當然茲詞 -&> 增加語法。

| 不茲詞 -&> 洗洗睡

而不是「在不方便的語言裡面強行柯里化好不好」,不喜輕拍。


如果你需要生成邏輯相似但是有些地方需要不同配置的函數的時候有用(不過這個跟柯里化好像關係不大,跟高階函數關係大些),例如生成綁定函數事件:

const changeGender = (gender) =&> () =&> user.gender = gender
$("input[value=male]").onChange(changeGender("male"))
$("input[value=female]").onChange(changeGender("female"))

其他情況你都可以用普通的函數替代,除非你想讓以後想打你的同事時候找到了恰到好處又不失大體的理由。


裝逼。

柯里化是因為 lambda 演算只有一個參數才被發明的,程序里用你除了噁心自己噁心別人並沒有什麼卵用。

一個業務邏輯你為什麼要拆成兩個函數,兩個業務邏輯你為什麼要叫一個功能。

還有說復用的,你輸入函數 A 輸出函數 B 給你復用這種不叫柯里化好嘛,你這麼做的唯一需求就是通過閉包讓函數 B 使用函數 A 執行期間相關的上下文而已,這本來就是 lambda 的普通使用方式,你輸入函數 A.1 輸出函數 A.2 繼續輸入函數 A.2 輸出結果才叫柯里化好嘛,柯里化的唯一用途就是實現多參。

如果你要問難道柯里化就只等於多參,計算機程序中的 function 不都是自帶多參的嗎,因為它倆本來就不是一個體系啊啊啊啊啊,現代計算機程序也只是借鑒了 lambda,這本來就不是在計算機程序平台誕生的概念,甚至要早於計算機之前,像閉包這種東西,每一種支持 lambda 的語言的傳教者都喜歡用閉包這個詞幌瞎初學者的狗眼,其實呢,就是一個訪問外部作用域變數的功能,跟沒有高階函數的語言訪問全局變數是一個邏輯性質,當然程序結構與生存周期上的差異你可以慢慢細講嘛,那麼喜歡扯,怎麼沒人來扯扯 α 變換和 β 代入呢,是因為和參數沒有什麼區別而沒逼可裝嗎?


可以對比下haskell這種天然的函數式語言,js里的珂里化實在是殘缺不全...

但是利用珂里化這種參數對應函數的思想,結合js的閉包特性,來實現良好的封裝。

舉個不恰當的例子:js中最常見的dom的插入和刪除。

普通寫法:變數都在外部,不能確保每個remove操作都「正確」的

var append = function(parent,child){
parent.appendChild(child);
}
var remove = function(dom){
dom.remove();
}
append(parent,child); //插入
remove(child); //刪除

文藝寫法:確保了每個刪除操作都會刪除插入的節點。

var append = function(parent,child){
parent.appendChild(child);
return function(){
child.remove();
}
}
//或者是這種,point free風格
var append2 = function(parent,child){
parent.appendChild(child);
return child.remove.bind(child);
}

var remove = append(parent,child);//插入一個節點,同時返回所插入的節點的刪除操作。
remove(); //刪除。

總結一下就是說,這種以函作為主體,確保了函數之間不會相互干擾,尤其是在複雜的前端工程下,每一處的代碼越「安全」,越獨立,越能更好的拓展功能和排查問題。

.


比如,對資料庫操作的基礎函數,封裝一下,傳參返回對某個特定資料庫的操作。再傳參,就是對某特定資料庫具體的增刪改查動作。

這當中每一層都是乾乾淨淨的封裝。


題主你的理解是對的……這個東西就是一個非常trivial的操作,它碰巧有個氣味撲鼻的名字而已……


柯里化,就是封裝『一系列的處理步驟』,這個是可以重用的;最後再把需要處理的東西傳進去。

js 上的話,推薦 http://ramdajs.com/ 這個庫,比 underscore 更加適合函數化編程(


1. 延遲計算

下面是一個部分求和的例子:

function currying(func) {
const args = [];
return function result(...rest) {
if (rest.length === 0)
return func(...args);

args.push(...rest);
return result;
}
}

const add = (...args) =&> args.reduce((a, b) =&> a + b);

const sum = currying(add);

sum(1,2)(3);
sum(4);
sum(); // 10

2. 動態創建函數

例如兼容現代瀏覽器和IE瀏覽器的添加事件方法,我們通常會這樣寫:

const addEvent = function (elem, type, fn, cature) {
if (window.addEventListener) {
elem.addEventListener(type, (e) =&> fn.call(elem, e), capture);
} else if (window.attachEvent) {
elem.attachEvent("on" + type, (e) =&> fn.call(elem, e);
}
}

這種方法顯然有個問題,就是每次添加事件處理都要執行一遍`if {...} else if {...}`。其實用下面的方法只需判斷一次即可:

const addEvent = (function () {
if (window.addEventListener) {
return (elem, type, fn, capture) =&> {
elem.addEventListener(type, (e) =&> fn.call(elem, e), capture);
};
} else {
return (elem, type, fn, capture) =&> {
elem.attachEvent("on" + type, (e) =&> fn.call(elem, e);
};
}
})();

這個例子,第一次`if {...} else if {...}`判斷之後,完成了部分計算,動態創建新的函數來處理後面傳入的參數,以後就不必重新進行計算了。這是一個典型的柯里化的應用。

3. 參數復用

當多次調用同一個函數,並且傳遞的參數絕大多數是相同的時候,那麼該函數就是一個很好的柯里化候選。

例如我們經常會用`Function.prototype.bind`方法來解決上述問題。

const obj = { name: "test" };
const foo = function (prefix, suffix) {
console.log(prefix + this.name + suffix);
}.bind(obj, "currying-");

foo("-function"); // currying-test-function

與`call`/`apply`方法直接執行不同,`bind`方法將第一個參數設置為函數執行的上下文,其他參數依次傳遞給調用方法(函數的主體本身不執行,可以看成是延遲執行),並動態創建返回一個新的函數。這很符合柯里化的特徵。下面來手動實現一下`bind`方法:

Function.prototype.bind = function (context, ...args) {
return (...rest) =&> this.call(context, ...args, ...rest);
};

4. 面試考點

正如上面的答主所說,柯里化和`call`, `apply`, `bind`有著千絲萬縷的聯繫,也會用到閉包的功能。所以實在是能衍生出很多的考題。


建議先問問你們團隊的人喜不喜歡這個,curry很好很強大,但是對於初學者有壓力,如果你們團隊的人員流動很快並且都入行不久,建議慎重。如果是老的穩定的牛團隊,可以考慮


為什麼感覺樓上那些大神都沒答到點子上呢?

強行答一下。

在我看來,工程上的柯里化主要就是為了統一介面,從而實現更高程度的抽象。

先從大家熟悉的概念說起。在我們用oop編程的時候,這層抽象通常是用對象模型,通過繼承並且使用基類的虛函數實現。通過繼承,我們得到了一個統一的介面,於是只要能夠適配基類的運算我們就可以相應的用子類來參與。

在使用functional範式的時候,實現這層抽象用的就是柯里化。運算不再是適配基類,而是適配特定函數signature。那萬一函數signature不同怎麼辦?柯里化唄。


在ES6就有用了,ECMAScript 6入門 查看thunk函數部分有真相(逃

----------------------------------------------------------------

2016.10.12

最近在寫React,經常在各種列表中使用柯里化生成綁定事件:

class ListComponent extends React.Component {
render = () =&> (& {data.map(d=&>&

  • &)}
    &)

    onClick=(data)=&>()=&>{doSomething(data)}
    }

    當然,我們還有其他方式來生成事件

    class ListComponent extends React.Component {
    render = () =&> (& {data.map(d=&>&

  • this.onClick(d)}&>&)}
    &)

    onClick=(data)=&>{doSomething(data)}
    }

    相對而言,我選擇柯里化,因為可以清楚的表現邏輯

    另外在React-redux里也有一處使用了柯里化,就是其提供的connect函數

    connect(mapStateToProps, mapDispatchToProps)(Counter)


    我跟妹子解釋了一遍在js裡面什麼叫柯里化以後,

    妹子:「很好,但是柯里化有什麼用呢?」

    我:「你看啊,柯里化以後呢,你可以自己實現一個可處理任意個數參數的函數,只要你像剛才我跟你講解的用遞歸方式寫的柯里化函數,你不就得到一個參數個數任意的處理函數了么,但是實際工程不會有人這麼寫的,這就是炫技加裝逼,面試的時候能夠回答上柯里化肯定會加分,BLABLABLA(省略現場編的3000字)。。。」

    嗯,對於認識淺薄的我,柯里化就是用來跟妹子吹牛用的。。


    最大的意義是有些庫作者可以宣稱自己的庫是「柯里化」的(還真見過不少),這樣一些萌新看到這個厲害的詞就會被多吸引住幾秒鐘目光。

    口號喊得亮在工程上可是非常有意義的。


    curry化就是一個一個傳參數

    和一起傳過去(tuple,參數列表,etc)能實現的語義是等價的。

    實際應用的話,除非你習慣數學那套語言,喜歡傳部分調用的東西。從一開始就堅持寫curry化的代碼。

    把現有的代碼curry化並不會帶來什麼直接的好處吧。畢竟都是等價的。


    想學好函數式還是去學一學haskell吧,去看一下趣學指南。


    bind context預綁定


    有方便的 lambda 之後,在工程上就沒啥用了


    別的語言不知道

    js就是面試用,主要就是考js的語法基礎

    實際在項目里如果不是想有意識的去用一下,一般不會想到它,只限於我參於的項目。

    現在直接問柯里化,但完全是醉翁之意不在酒

    因為柯里化能很方便的引申出js的幾項語法要點

    arguments

    閉包

    call/apply-&>bind-&>this-&>面向對象

    這幾個能答的差不多,語法就算過關了。


    對比一下:

    var E = function () {};
    E.prototype.scold = function (msg) { alert(msg); return this; };
    new E().scold("I"m angry!").scold("Too young!");

    var E = function () {
    var simple = function (msg) { alert(msg); return simple; };
    return simple;
    };
    E()("I"m angry!")("Too young!");

    // 如果你想要「真正」的柯里化……
    var E = function () {
    for (let msg of arguments) {
    alert(msg);
    }
    };
    E.bind(null, "I"m angry!").bind(null, "Too young!")();


    推薦閱讀:
  • 什麼是函數式語言?
    有哪些值得深入學習RxAndroid的開源項目?
    EDSL相關雜記(1)
    怎樣理解「組合優於繼承」以及「OO的反模塊化」,在這些方面FP具體來說有什麼優勢?
    haskell中的類型類是相當於面向對象語言的介面嗎?

    TAG:前端開發 | JavaScript | 函數式編程 |