朋友,柯里化(Currying)瞭解一哈

該文章是直接翻譯國外一篇文章,關於柯里化(Currying)。
都是基於原文處理的,其餘的都是直接進行翻譯可能有些生硬,因此爲了行文方便,就作了一些簡單的本地化處理。
若是想直接根據原文學習,能夠忽略此文。javascript

若是你以爲能夠,請多點贊,鼓勵我寫出更精彩的文章🙏。java

案例引導:

如今有以下需求,將多參函數轉換爲n個單參函數的組合。若是沒有想到合適的方式來實現,巧了不是嘛,這不是巧了嘛。這篇文章就是爲了說明白這個問題的。spring

Talk is cheap,show you the code

//原函數
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:

  1. 你能夠選擇一次傳入多個參數。這中處理方式在開發應用中頗有用。
  2. 正如上面聲明,你能夠在調用函數過程當中,傳入空參,它也會返回一個等待剩餘參數的函數

手動實現一個柯里化函數

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]);
  };
};
複製代碼

打開控制檯,咱們來一塊兒欣賞一下這段奇妙的代碼。

準備工做

greetcurry複製到控制檯。而後輸入curriedGreet = curry(greet),而後開啓這段奇妙之旅吧。

第一次停頓(代碼第二行)

經過監聽函數的兩個參數,咱們能夠看到 originalFunction就是 greet而且因爲咱們沒有提供第二個參數,因此 initialParams的值仍是在定義函數時候的默認值。移動到下一個斷點處。

猛然發現斷點直接跳出函數做用域,也就是curry(greet)返回了一個等待(N>3)的函數。在控制檯判斷返回值的類型,也和咱們分析的同樣。

而且咱們繼續調用返回的函數,並存於sayHello變量中。

sayHello = curriedGreet('Hello')
複製代碼

而且在控制檯執行它。

第二次停頓(代碼第四行)

在進行第二次停頓以前,咱們查看了監聽的 originalFunctioninitialParams。可是在第一次斷點以後,就返回了一個 函數,爲何在新函數的做用域中,也能夠訪問到這些變量呢?這是由於該新函數是從父級函數中返回的,可以訪問父級函數中定義變量。(或者用更加通俗的話來說,這是 閉包,關於閉包,會專門有一篇文章,進行講解,如今在籌備過程當中。敬請期待)

父子函數之間的參數繼承關係

當一個父級函數調用以後,他們會將本身參數留給子孫級函數所使用。這種繼承方式和現實方式中繼承是同樣的。

curry在定義/初始化的時候,就將originalFunctioninitialParams做爲初始參數,隨後返回了一個子函數(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']。因而可知,initialParamsnextParams都爲數組,因此,能夠經過spread operator將兩個數組進行合併處理。

關鍵點,就在這裏。

若是 params and originalFunction具備相同長度,將會直接調用 greet,也就意味着柯里化過程完成了。

JS函數也具備length屬性

這也是可以完成柯里化的關鍵步驟。此處就判斷返回的函數是否繼續等待剩餘參數。(提早透露下,這裏是結束遞歸的判斷,若是沒有這個,將直接致使死循環)

在JS中,一個函數的.length屬性用於標識在函數定義的時候,有幾個參數。或者說,函數期待幾個參數參與函數運行。

greet.length; // 3

iTakeOneParam = (a) => {};
iTakeTwoParams = (a, b) => {};

iTakeOneParam.length; // 1
iTakeTwoParams.length; // 2

複製代碼

若是你提供了函數指望的參數個數,那柯里化工做直接返回原始函數而且不在進行其他操做。

可是,咱們提供的示例中,parameters的長度是和函數長度不同的。咱們僅僅提供了Hello,因此parameters長度爲1,可是originalFunction.length3。因此此處的if()判斷是false。咱們將走的是另一個分支。從新調用主函數curry(也就是進入了遞歸處理了),而此時curry()接收greetHello爲參數,從新走上面的流程。

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.length2仍是沒有達到預期的數值,繼續遞歸處理。

||
                                        ||
                                        ||
                                        \/
複製代碼

因此咱們繼續基於sayHelloToFan進行處理。sayHelloToFanBeichen = sayHelloToFan('北宸')

繼續開始了參數處理和參數判斷之旅。可是此時有一點不一樣了。就是在判斷參數數組長度和originalFunction.length時候。

||
                                    ||
                                    ||
                                    \/
複製代碼

此時的話,就和調用greet('Hello','範','北宸')的效果和結果是同樣的。

大結局

greet獲取了它應獲取的參數,curry也中止了遞歸處理。而且,咱們也得到了想要的結果Hello,範北宸

其實利用currygreet通過如上處理以後,如今處理以後的函數可以同時接收任意(n≥0)的參數。

curriedGreet('Hello', '範', '北宸');
curriedGreet('Hello', '範')('北宸');
curriedGreet()()('Hello')()('範')()()()()('北宸');

複製代碼

補丁

因爲在這篇文章發文以後,有一些小夥伴問,curry的好處也好啊,仍是如何應用到實際工做中啊。其實這篇文章只是單純的介紹如何用JS實現curry

而有一點須要你們明確,curry函數式編程中的一個重要概念。若是說實際中用到這個編程方式了嗎,說實話,我沒有。可是通過翻閱一些資料,打算之後項目中會嘗試使用。

因此,給你們列舉一下我查找的相關資料(其實就是函數式編程的官網的一些介紹文章)

  1. 愛上柯里化
  2. 爲何柯里化有幫助
  3. 手動實現一個Redux Redux是React項目開發中比較經常使用的一個庫。
相關文章
相關標籤/搜索