Currying 及應用


Currying,中文多翻譯為柯里化,感覺這個音譯還沒有達到類似 Humor 之於幽默的傳神地步,后面直接使用 Currying。

什么是 Currying

Currying 是這么一種機制,它將一個接收多個參數的函數,拆分成多個接收單個參數的函數。

考察下面的代碼:

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

add(3, 4); // returns 7

add 接收兩個參數 ab,並返回它們的和 a+b

經過 curry 化處理后,函數成了如下形式:

function add (a) {
  return function (b) {
    return a + b;
  }
}

現在 add 接收一個參數 a,返回另一個接收一個參數 b 的函數。

add(3)(4);

var add3 = add(3);

add3(4);

現在當調用 add(3) 后,得到的不是和,而是另一個接收一個參數的函數,因此,add 的返回可以繼續被調用,add(3)(4) 后面的這次調用才會將 4 加到 3 上得到和。

var add3 = add(3) 這樣的單次調用,得到的函數效果相當於是將 3 保存在了新函數的閉包中,該函數會對傳入的參數加 3。

注意這里提到了將入參 3 保存 到了閉包中后續使用,很容易聯想到 Function.prototype.bind(),它就可以對傳入的函數提前綁定一些預設的入參:

function.bind(thisArg[, arg1[, arg2[, ...]]])

后面會看到,正因為 bind 和 Currying 有點關系,在實現任意函數的 Currying 化時會用到它。

注意到 Currying 化的定義,其實是將多個參數打散到多個函數中,這個過程可通過代碼來自動化,以達到將任意多入參函數進行 Currying 化的目的,后面討論實現。

偏函數/Partial Application

區別與 Currying,如果在拆分入參的過程中,這些拆分出來的函數不是一次只應用其中的一個,而是任意多個,則這些函數就是部分應用(Parital application)了原函數中的入參,稱作偏函數。

考察下面的 add 函數,其實是將前面示例中的 add 入參進行了擴充,由兩個增加到四個:

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

那么如下的函數就都是偏函數,它們都部分應用了 add 的入參:

function partial1(a) {
  return function(c) {
    return a + b + c + d;
  };
}
function partial2(a, b) {
  return function(c, d) {
    return a + b + c + d;
  };
}
function partial3(a, b, c) {
  return function(d) {
    return a + b + c + d;
  };
}

偏函數中這種入參的拆分和部分應用,並不僅限於一層的拆分,可以是任意多次的:

function partial1(a, b) {
  return function partial2(c) {
    return function partial3(d) {
      return a + b + c + d;
    };
  };
}

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

其中,partial1partial2partial3 一起構成了原 add 函數的偏函數。

可以看到,偏函數是 Curring 更加一般(general)的形式,下面看如何實現將任意函數進行 Currying 化,或偏函數化。

將一般化函數進行 Currying 化

我們需要構造這么一個函數假設名叫 curry

function curry(fn){
  // 待實現
}

調用 curry 后,我們可以得到原函數 Curry 化后的版本,

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

var currified = curry(add);

即上述 currified 應該等效為:

function currified (a) {
  return function (b) {
    return a + b;
  }
}

首先,通過 Function.length 是可以知道一個給定函數其預期的入參個數的。

再加上前面提到的 bind 函數,可以得到如下的實現:

function curry(f) {
  return function currify() {
    const args = Array.prototype.slice.call(arguments);
    return args.length >= f.length ?
      f.apply(null, args) :
      currify.bind(null, ...args)
  }
}

下面測試一下:

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

var currified = curry(add);

currified(1)(2); // 3

並且以上實現不只是簡單的 Currying 化,可以是任意數量和任意次數的 parial application:

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

var currified = curry(add);

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

總之就是各種形狀hàn姿勢,各種顏色hàn皮膚的組合。

自動化的 CurryIng 倒是實現了,可說了半天,它具體有什么實用價值。

函數的組合(function composition)

我們知道代數里面可以有函數的組合,譬如:

f(x) = x * x
g(y) = y + 1
g(f(x)) = x * x + 1

g(f(2)) = 2 * 2 + 1 = 5

上述代數表達轉成 JavaScript 即:

function f(x) {
  return x ** 2;
}

function g(y) {
return y + 1;
}

g(f(2)) // 5

這里用到了兩個函數 fg 聯合起來得到一個結果,他們都分別只接收一個入參同時返回一個結果。

像這樣只接收一個入參並返回一個結果的函數,便符合組裝的需求,可像上面這樣自由組合。通過上面的討論我們知道,任意函數都可經過 Currying 化處理后變成多個只接收單個入參的函數。這就為函數的組合提供了基礎。

因此我們可以將 fg 的結合形成一個新的函數,這個函數作為對外的接口被調用即可。

const compose = fn1 => fn2 => input => fn1(fn2(input));

使用:

const myFn = compose(f)(g);
myFn(2); // 5

像上面的 compose 還不夠一般化,他只接收兩個函數並對其進行結合,下面來看更加一般化的函數組合,將實現接收任意多個函數。

const pipe = (...fns) => input => fns.reduce((mem, fn) => fn(mem), input)

const double = x => x 2
const addOne = x => x + 1
const square = x => x
x

pipe(square, double, addOne)(2)

上面的 pipe 將對輸入依次應用 入參中的各函數,所以取名 pipe 管道流。

以上,函數的組裝。

相關資源


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM