該文章是直接翻譯國外一篇文章,關於柯里化(Currying)。
都是基於原文處理的,其餘的都是直接進行翻譯可能有些生硬,因此爲了行文方便,就作了一些簡單的本地化處理。
若是想直接根據原文學習,能夠忽略此文。javascript
若是你以爲能夠,請多點贊,鼓勵我寫出更精彩的文章🙏。java
如今有以下需求,將多參函數轉換爲n個單參函數的組合。若是沒有想到合適的方式來實現,巧了不是嘛,這不是巧了嘛。這篇文章就是爲了說明白這個問題的。spring
//原函數
add=(first,second,third)=>first+second+third;
//函數改造
add(1)(2)(3) //6
add(1,2)(3) //6
add(1)()()(2,3) //6
複製代碼
咱們先來看一下關於Currying的定義(該定義被計算機科學和數學都承認)編程
(Currying將多參函數轉換爲單參函數)
Currying turns multi-argument functions into unary (single argument) functions.redux
被柯里化的原函數,一次能夠接收多個參數。就像下面同樣:數組
greet = (greeting, first, last) => `${greeting}, ${first} ${last}`;
greet('Hello', '範', '北宸'); // Hello, 範北宸
複製代碼
對原函數(greet
)進行適當的柯里化處理以後閉包
curriedGreet = curry(greet);
curriedGreet('Hello')('範')('北宸'); // Hello, 範北宸
複製代碼
這個三元函數已經被改造爲三個一元函數。當你提供了一個參數,一個期待下一個參數的新的函數被返回。函數式編程
上面之因此說適當的柯里化是由於一些柯里化函數在使用的時候是很是靈活的。柯里化偉大之處在於理論思惟,可是在JS中爲構建/調用一個函數爲了處理每一個參數將變的很棘手。函數
Ramda’s 柯里化函數可讓你經過下面的方式來調用curriedGreet
:post
// greet 須要三個參數: (greeting, first, last)
// 這些都將返回一個函數,等待剩餘參數(first, last)
curriedGreet('Hello');
curriedGreet('Hello')();
curriedGreet()('Hello')()();
// 這些都將返回一個函數,等待剩餘參數(last)
curriedGreet('Hello')('範');
curriedGreet('Hello', '範');
curriedGreet('Hello')()('範')();
// 當參數個數符合最初定義的時候,將會返回最後結果,這些將返回一個字符串
curriedGreet('Hello')('範')('北宸');
curriedGreet('Hello', '範', '北宸');
curriedGreet('Hello', '範')()()('北宸');
複製代碼
Notice:
Mr. Elliot 分享了一個和Ramda
相似的curry
實現。
const curry = (f, arr = []) => (...args) =>
((a) => (a.length === f.length ? f(...a) : curry(f, a)))([...arr, ...args]);
複製代碼
是否是很驚奇。心中是否萬馬奔騰。這玩意就能實現curry
。
將ES6的箭頭函數替換爲更加看懂的方式,同時新增了debugger
,便於在分析調用過程。
curry = (originalFunction, initialParams = []) => {
debugger;
return (...nextParams) => {
debugger;
const curriedFunction = (params) => {
debugger;
if (params.length === originalFunction.length) {
return originalFunction(...params);
}
return curry(originalFunction, params);
};
return curriedFunction([...initialParams, ...nextParams]);
};
};
複製代碼
打開控制檯,咱們來一塊兒欣賞一下這段奇妙的代碼。
將greet
和curry
複製到控制檯。而後輸入curriedGreet = curry(greet)
,而後開啓這段奇妙之旅吧。
originalFunction
就是
greet
而且因爲咱們沒有提供第二個參數,因此
initialParams
的值仍是在定義函數時候的默認值。移動到下一個斷點處。
猛然發現斷點直接跳出函數做用域,也就是curry(greet)
返回了一個等待(N>3)的函數。在控制檯判斷返回值的類型,也和咱們分析的同樣。
而且咱們繼續調用返回的函數,並存於sayHello
變量中。
sayHello = curriedGreet('Hello')
複製代碼
而且在控制檯執行它。
originalFunction
和
initialParams
。可是在第一次斷點以後,就返回了一個
新函數,爲何在新函數的做用域中,也能夠訪問到這些變量呢?這是由於該新函數是從父級函數中返回的,可以訪問父級函數中定義變量。(或者用更加通俗的話來說,這是
閉包,關於閉包,會專門有一篇文章,進行講解,如今在籌備過程當中。敬請期待)
當一個父級函數調用以後,他們會將本身參數留給子孫級函數所使用。這種繼承方式和現實方式中繼承是同樣的。
curry
在定義/初始化的時候,就將originalFunction
和initialParams
做爲初始參數,隨後返回了一個子函數(child)。所以這兩個變量沒有被銷燬,由於子函數也對其有訪問權限。
經過監聽nextParams
咱們突然發現,該值爲['Hello']
。可是咱們在調用curriedGreet()
的時候,是傳入的'Hello'
而不是['Hello']
。
謎底:咱們在調用curriedGreet
的時候,傳入的是'Hello'
,可是經過rest syntax,咱們將'Hello'
轉換爲['Hello']
。
curry 是一個能夠接收N(N>1,10,100)個參數的函數,因此經過rest syntax
處理以後,函數可以輕鬆的訪問這些參數。既然每次都是傳入一個參數,經過rest syntax
每次都將參數捕獲到數組中。
繼續移動斷點。
在運行第六行的debugger
以前,是先調用12行的。咱們在第五行定義了一個名爲curriedFunction
的函數,在12行處調用他。因此咱們將斷點放置在了方法體內。那調用curriedFunction
的時候,傳入的數據是啥呢?
[...initialParams, ...nextParams];
複製代碼
在第五行咱們查看了參數...nextParams
爲['Hello']
。因而可知,initialParams
和nextParams
都爲數組,因此,能夠經過spread operator
將兩個數組進行合併處理。
關鍵點,就在這裏。
params
and
originalFunction
具備相同長度,將會直接調用
greet
,也就意味着柯里化過程完成了。
這也是可以完成柯里化的關鍵步驟。此處就判斷返回的函數是否繼續等待剩餘參數。(提早透露下,這裏是結束遞歸的判斷,若是沒有這個,將直接致使死循環)
在JS中,一個函數的.length
屬性用於標識在函數定義的時候,有幾個參數。或者說,函數期待幾個參數參與函數運行。
greet.length; // 3
iTakeOneParam = (a) => {};
iTakeTwoParams = (a, b) => {};
iTakeOneParam.length; // 1
iTakeTwoParams.length; // 2
複製代碼
若是你提供了函數指望的參數個數,那柯里化工做直接返回原始函數而且不在進行其他操做。
可是,咱們提供的示例中,parameters
的長度是和函數長度不同的。咱們僅僅提供了Hello
,因此parameters
長度爲1,可是originalFunction.length
爲3。因此此處的if()
判斷是false
。咱們將走的是另一個分支。從新調用主函數curry
(也就是進入了遞歸處理了),而此時curry()
接收greet
和Hello
爲參數,從新走上面的流程。
curry
本質上是一個無限循環的自我調用而且對參數貪婪的函數,直到函數個數===originalFunction.length
纔會中止。
這是在對greet
進行柯里化處理時的參數快照curriedGreet = curry(greet)
這是在greet
柯里化以後而且接受一個參數以後的參數快照sayHello = curriedGreet('Hello')
很顯然,第二次運行到第二行的時候,參數變化了,也就是originalFunction
仍是greet
,可是如今initialParams
變成了['Hello']
了,而不是空數組了。
而後繼續跳過斷點,又雙叒叕返回了一個全新的函數(sayHello
),而這個函數也期待這剩餘函數的傳入。 sayHelloToFan = sayHello('範').
繼續跟蹤斷點,又跳到第四行,此時nextParams
爲['範']
。
curriedFunction
的參數爲
['Hello', '範']
在12行有進行數組合並的處理[...initialParams, ...nextParams]
,而initialParams
爲[Hello]
,nextParams
是經過...nextParams
操做以後,將範
轉換爲['範']
。因此,在12行的時候,就是針對兩個數組進行合併處理[...['Hello'],...['範']
。
如今又到了curriedFunction
抉擇的時候了,此時params.length
爲2仍是沒有達到預期的數值,繼續遞歸處理。
||
||
||
\/
複製代碼
因此咱們繼續基於sayHelloToFan
進行處理。sayHelloToFanBeichen = sayHelloToFan('北宸')
繼續開始了參數處理和參數判斷之旅。可是此時有一點不一樣了。就是在判斷參數數組長度和originalFunction.length
時候。
||
||
||
\/
複製代碼
此時的話,就和調用greet('Hello','範','北宸')
的效果和結果是同樣的。
greet
獲取了它應獲取的參數,curry
也中止了遞歸處理。而且,咱們也得到了想要的結果Hello,範北宸
。
其實利用curry
對greet
通過如上處理以後,如今處理以後的函數可以同時接收任意(n≥0)的參數。
curriedGreet('Hello', '範', '北宸');
curriedGreet('Hello', '範')('北宸');
curriedGreet()()('Hello')()('範')()()()()('北宸');
複製代碼
因爲在這篇文章發文以後,有一些小夥伴問,curry
的好處也好啊,仍是如何應用到實際工做中啊。其實這篇文章只是單純的介紹如何用JS實現curry
。
而有一點須要你們明確,curry
是函數式編程中的一個重要概念。若是說實際中用到這個編程方式了嗎,說實話,我沒有。可是通過翻閱一些資料,打算之後項目中會嘗試使用。
因此,給你們列舉一下我查找的相關資料(其實就是函數式編程的官網的一些介紹文章)