2021 年 1 月 10 日

柯里化

柯里化是一種進階的函式處理技巧。它不只用於 JavaScript,也用於其他語言。

柯里化是一種函式轉換,它將一個可呼叫為 f(a, b, c) 的函式轉換為可呼叫為 f(a)(b)(c) 的函式。

柯里化不會呼叫函式。它只會轉換函式。

讓我們先看一個範例,以更了解我們在討論什麼,然後再看實際應用。

我們將建立一個輔助函式 curry(f),它會為一個雙引數 f 執行柯里化。換句話說,curry(f) 會將雙引數 f(a, b) 轉換為一個執行為 f(a)(b) 的函式

function curry(f) { // curry(f) does the currying transform
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// usage
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert( curriedSum(1)(2) ); // 3

如您所見,實作很簡單:它只有兩個包裝函式。

  • curry(func) 的結果是一個包裝函式 function(a)
  • 當它被呼叫為 curriedSum(1) 時,引數會儲存在詞法環境中,並傳回一個新的包裝函式 function(b)
  • 然後這個包裝器會以 2 作為參數被呼叫,並將呼叫傳遞給原始的 sum

更進階的 currying 實作,例如 lodash 函式庫中的 _.curry,會回傳一個包裝器,允許函式同時以一般和部分的方式被呼叫。

function sum(a, b) {
  return a + b;
}

let curriedSum = _.curry(sum); // using _.curry from lodash library

alert( curriedSum(1, 2) ); // 3, still callable normally
alert( curriedSum(1)(2) ); // 3, called partially

Currying?用來幹嘛的?

要了解它的好處,我們需要一個有價值的真實範例。

例如,我們有一個記錄函式 log(date, importance, message),用於格式化和輸出資訊。在實際專案中,此類函式有許多有用的功能,例如透過網路傳送記錄,但我們在此僅使用 alert

function log(date, importance, message) {
  alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

讓我們來做 currying!

log = _.curry(log);

這樣之後,log 就可以正常運作了

log(new Date(), "DEBUG", "some debug"); // log(a, b, c)

…也可以使用 currying 形式

log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

現在,我們可以輕鬆地為目前的記錄建立一個便利函式

// logNow will be the partial of log with fixed first argument
let logNow = log(new Date());

// use it
logNow("INFO", "message"); // [HH:mm] INFO message

現在,logNow 是具有固定第一個參數的 log,換句話說,就是「部分套用函式」或簡稱「部分函式」。

我們可以更進一步,為目前的除錯記錄建立一個便利函式

let debugNow = logNow("DEBUG");

debugNow("message"); // [HH:mm] DEBUG message

所以

  1. 在進行 currying 之後,我們並沒有失去任何東西:log 仍然可以正常呼叫。
  2. 我們可以輕鬆地產生部分函式,例如針對今日的記錄。

進階的 curry 實作

如果你想深入了解細節,以下是我們可以在上面使用的進階 curry 實作,適用於多個參數的函式。

它很簡短

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

使用範例

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6, still callable normally
alert( curriedSum(1)(2,3) ); // 6, currying of 1st arg
alert( curriedSum(1)(2)(3) ); // 6, full currying

新的 curry 看起來可能很複雜,但實際上很容易理解。

curry(func) 呼叫的結果是包裝器 curried,它看起來像這樣

// func is the function to transform
function curried(...args) {
  if (args.length >= func.length) { // (1)
    return func.apply(this, args);
  } else {
    return function(...args2) { // (2)
      return curried.apply(this, args.concat(args2));
    }
  }
};

當我們執行它時,會有兩個 if 執行分支

  1. 如果傳遞的 args 數量與原始函式在定義中擁有的數量相同或更多(func.length),則使用 func.apply 將呼叫傳遞給它。
  2. 否則,取得一個部分函式:我們還沒有呼叫 func。相反地,會回傳另一個包裝器,它將重新套用 curried,提供先前的參數和新的參數。

然後,如果我們再次呼叫它,我們將會得到一個新的部分(如果參數不足)或最終結果。

僅限固定長度的函式

柯里化要求函式具有固定數量的參數。

使用剩餘參數的函式,例如 f(...args),無法以此方式進行柯里化。

比柯里化多一點

根據定義,柯里化應將 sum(a, b, c) 轉換為 sum(a)(b)(c)

但是,JavaScript 中大多數柯里化實現都是進階的,如所述:它們還使函式在多參數變體中可呼叫。

摘要

柯里化是一種轉換,可將 f(a,b,c) 呼叫為 f(a)(b)(c)。JavaScript 實作通常會同時保持函式正常呼叫,並在參數數量不足時傳回部分函式。

柯里化允許我們輕鬆取得部分函式。正如我們在記錄範例中所見,在對三參數通用函式 log(date, importance, message) 進行柯里化後,在呼叫時提供一個參數(例如 log(date))或兩個參數(例如 log(date, importance))時,會提供部分函式。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改進建議,請 提交 GitHub 議題 或提交 Pull Request,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,若要插入多行程式碼,請將它們包覆在 <pre> 標籤中,若要插入超過 10 行的程式碼,請使用沙盒(plnkrjsbincodepen…)