[譯] 柯里化與函數組合

煙霧藝術從方塊到煙霧 — MattysFlicks — (CC BY 2.0)javascript

注意:此篇文章是「組合軟件」系列的一部分,這個系列的目的是從頭在 JavaScript ES6+ 環境下學習函數式編程和組合軟件技術。敬請關注。咱們會講述大量關於這方面的知識! < 上一篇 | << 第一篇前端

隨着在主流 JavaScript 中函數式編程戲劇般地興起,在許多應用中柯里化函數變得廣泛起來。理解它們是什麼、如何運做和怎樣有效地運用很是重要。java

什麼是柯里化函數?

柯里化函數是一種由須要接受多個參數的函數轉化爲一次只接受一個參數的函數。若是一個函數須要 3 個參數,那柯里化後的函數會接受一個參數並返回一個函數來接受下一個參數,這個函數返回的函數去傳入第三個參數。最後一個函數會返回應用了全部參數的函數結果。android

你能夠用更多或更少數量的參數來作一樣的事。例若有兩個數字,ab 的柯里化形式會返回 ab 之和。ios

// add = a => b => Number
const add = a => b => a + b;
複製代碼

爲了使用它,咱們必須使用函數應用語法應用到這兩個函數上。在 JavaScript 中,函數後的括號 () 觸發函數調用。當函數返回另外一個函數,被返回的函數能夠經過一對額外的括號被當即調用:git

const result = add(2)(3); // => 5
複製代碼

首先,函數接受參數 a返回一個新的函數,新函數接受 b 返回 ab 之和。一次接受一個參數。若是函數有更多參數,它會簡單地繼續返回新函數直到全部的參數都被提供,這時應用完成。es6

add 函數接受一個參數,而後返回本身的 偏函數應用a 固定在偏函數應用的閉包做用域中。閉包指函數綁定其語法做用域。閉包在建立函數運行時被建立。固定意味着在閉包綁定的做用域內變量被賦值。github

上例中的括號表明的函數調用過程:使用 2 作參數調用 add,返回偏函數應用而且 a 的值固定爲 2。咱們不會將返回值賦值給變量或以其餘方式使用它,而是經過在括號中將 3 傳遞給它來當即調用返回函數,從而完成應用並返回 5編程

什麼是偏函數應用(Partial Application)?

偏函數應用是指使用一個函數並將其應用一個或多個參數,但不是所有參數。換句話說,它是一種在閉包做用域中已擁有一些固定參數的函數。偏函數應用是擁有部分固定參數的函數。後端

它們之間的不一樣之處?

偏函數應用能夠根據須要一次接受多或少的參數。而柯里化函數老是返回一元函數:函數老是接受一個參數

全部的柯里化函數都返回偏函數應用,但不是全部的偏函數應用都是柯里化函數的結果。

柯里化函數的一元需求是一個重要特性。

什麼是無點風格(point-free style)?

無點風格是一種編程風格,其函數定義不會關聯函數的參數。讓咱們來看 JavaScript 中的函數定義:

function foo (/* 這裏定義參數*/) {
  // ...
}

const foo = (/* 這裏定義參數 */) => // ...

const foo = function (/* 這裏定義參數 */) {
  // ...
}
複製代碼

你如何能在 JavaScript 中定義不關聯參數的函數?咱們不能使用 function 關鍵字,也不能使用箭頭函數(=>),由於這些都要求正式的參數聲明。因此咱們要作的是調用一個會返回函數的函數。

使用無點風格建立一個函數,該方法會把你傳入的任何數字加一。記住,咱們已經有一個叫 add 的函數,它須要一個數字作參數,而且不管你傳入了什麼值都會返回一個第一個參數固定的偏函數。咱們可使用這種方法建立一個叫 inc() 的新函數。

// inc = n => Number
// 把任何數字加一。
const inc = add(1);

inc(3); // => 4
複製代碼

做爲一種泛化和專用機制,這頗有趣。返回的函數不過是更加通用的 add() 函數的一種專用版。咱們能夠按須要使用 add() 來建立許多專用版本。

const inc10 = add(10);
const inc20 = add(20);

inc10(3); // => 13
inc20(3); // => 23
複製代碼

固然,全部這些都有它們本身的閉包做用域(閉包在函數建立時被建立 —— 在 add() 被調用時),因此原來 inc() 能夠保持功能:

inc(3) // 4
複製代碼

當咱們調用 add(1) 來建立 inc() 時,add() 中的 a 參數在返回的函數中固定爲 1,這個返回的函數賦值給inc

當咱們調用 inc(3) 時,add() 中的 b 參數被參數 3 替換,函數結束,返回 13 之和。

全部的柯里化函數都是高階形式函數,它容許你爲了專門用途建立原函數的專用版本。

爲何要把函數柯里化?

柯里化函數在函數組合中極其有用。

在代數學中,假設有兩個函數,fg

f: a -> b
g: b -> c
複製代碼

你能夠把這兩個函數組合來建立一個新函數 h,從 a 直接獲得 c

// 代數定義,從 Haskell 借鑑了組合操做符 `.`

h: a -> c
h = f . g = f(g(x))
複製代碼

在 JavaScript 中:

const g = n => n + 1;
const f = n => n * 2;

const h = x => f(g(x));

h(20); //=> 42
複製代碼

代數定義:

f . g = f(g(x))
複製代碼

能夠被轉換成 JavaScript:

const compose = (f, g) => f(g(x));
複製代碼

但這隻能一次組合兩個函數。在代數中,有可能這麼寫:

g . f . h
複製代碼

咱們能夠隨意把任意多個函數組合成一個函數。換句換說,compose() 在函數中建立了一個管道,把一個函數的輸出與下一個函數的輸入鏈接起來。

我常常以這種方法來寫:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
複製代碼

此版本使用任意多個函數並返回一個須要初始值的函數,而後使用 reduceRight() 從右到左遍歷每個函數,即 fns 中的 f,並把它變成累積值 y。函數中累加器的計算值 y 就是函數 compose() 的返回值。

如今咱們能夠這樣組合:

const g = n => n + 1;
const f = n => n * 2;

// 使用 `compose(f, g)` 替換 `x => f(g(x))` `
const h = compose(f, g);

h(20); //=> 42
複製代碼

跟蹤(Trace)

函數組合使用無點風格建立很是簡潔易懂的代碼,不過若想簡單的調試則要花點功夫。若是你想檢查函數間的值?你可使用一種方便的工具 trace()。它須要柯里化函數的形式:

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
複製代碼

如今咱們來檢查管道:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};

const g = n => n + 1;
const f = n => n * 2;

/*
注意:函數應用的順序是從下到上:
*/

const h = compose(
  trace('after f'),
  f,
  trace('after g'),
  g
);

h(20);
/*
after g: 21
after f: 42
*/
複製代碼

compose() 是很是有用的工具,但當咱們須要組合多於兩個函數時,從上到下的順序會更方便咱們閱讀。咱們能夠經過反轉被調用函數的順序來作到。這裏有另外一個名爲 pipe 的組合工具,它反轉了組合的順序:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
複製代碼

如今咱們能夠這樣寫上面的代碼:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};

const g = n => n + 1;
const f = n => n * 2;

/*
如今函數應用的順序是從上到下:
*/
const h = pipe(
  g,
  trace('after g'),
  f,
  trace('after f'),
);

h(20);
/*
after g: 21
after f: 42
*/
複製代碼

結合柯里化和函數組合

即使不在函數組合的範疇中講,柯里化無疑也是一種很是有用的抽象,咱們能夠運用到專用函數。例如,柯里化版本的 map 能夠被專用化來作不少不一樣的事情:

const map = fn => mappable => mappable.map(fn);

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const log = (...args) => console.log(...args);

const arr = [1, 2, 3, 4];
const isEven = n => n % 2 === 0;

const stripe = n => isEven(n) ? 'dark' : 'light';
const stripeAll = map(stripe);
const striped = stripeAll(arr);
log(striped);
// => ["light", "dark", "light", "dark"]

const double = n => n * 2;
const doubleAll = map(double);
const doubled = doubleAll(arr);
log(doubled);
// => [2, 4, 6, 8]
複製代碼

可是柯里化函數的真正能力是它們能夠簡化函數組合。一個函數能夠接受任意數量的輸入,可是隻返回一個輸出。爲了使函數可組合,輸出類型必須與指望輸入類型統一:

f: a => b
g:      b => c
h: a    =>   c
複製代碼

若是上面的函數 g 指望兩個參數,f 的輸出就會和 g 的輸入不一致:

f: a => b
g:     (x, b) => c
h: a    =>   c
複製代碼

在這種狀況下如何把 x 傳入 g,答案是g 柯里化

記住柯里化函數的定義:一種由須要多個參數的函數轉化爲一次只接受一個參數的函數,而且經過使用第一個參數並返回一系列函數直到全部的參數都已被收集。

上述定義的關鍵詞是「一次傳入一個參數」。對於函數組合來講柯里化函數如此方便的緣由是它們把須要多個參數的函數變成了只須要一個參數的函數,容許它們適配函數組合管道。拿前面的 trace() 函數爲例:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};

const g = n => n + 1;
const f = n => n * 2;

const h = pipe(
  g,
  trace('after g'),
  f,
  trace('after f'),
);

h(20);
/*
after g: 21
after f: 42
*/
複製代碼

trace() 定義兩個參數,可是每次只取一個參數,容許咱們專用化行內函數。若是 trace() 沒有被柯里化,就不能這樣使用它。咱們就必須這樣寫管道函數:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const trace = (label, value) => {
  console.log(`${ label }: ${ value }`);
  return value;
};

const g = n => n + 1;
const f = n => n * 2;

const h = pipe(
  g,
  // trace() 不在是無點風格,並引入 `x` 做爲中間變量。
  x => trace('after g', x),
  f,
  x => trace('after f', x),
);

h(20);
複製代碼

可是單純的柯里化函數仍然不夠。你還須要保證函數指望的參數以按正確的順序來專用化它們。再看一遍咱們柯里化 trace() 時發生了什麼,不過此次咱們反轉參數的順序:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const trace = value => label => {
  console.log(`${ label }: ${ value }`);
  return value;
};

const g = n => n + 1;
const f = n => n * 2;

const h = pipe(
  g,
  // trace() 不能爲無點風格,由於指望的參數順序錯誤
  x => trace(x)('after g'),
  f,
  x => trace(x)('after f'),
);

h(20);
複製代碼

若是有必要,你可使用 flip 方法來解決這個問題,它簡單地反轉了兩個參數的順序:

const flip = fn => a => b => fn(b)(a);
複製代碼

如今咱們能夠建立 flippedTrace() 函數:

const flippedTrace = flip(trace);
複製代碼

並這樣使用它:

const flip = fn => a => b => fn(b)(a);
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const trace = value => label => {
  console.log(`${ label }: ${ value }`);
  return value;
};
const flippedTrace = flip(trace);

const g = n => n + 1;
const f = n => n * 2;

const h = pipe(
  g,
  flippedTrace('after g'),
  f,
  flippedTrace('after f'),
);

h(20);
複製代碼

不過更好的方式是在開始就寫出正確的函數。有時這種風格被稱爲「數據置後」,這意味着你須要首先傳入專用化參數,並在最後傳入參數執行函數。這裏展現了原始的函數形式:

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};
複製代碼

trace() 每次應用 label 時會建立專用版本的跟蹤函數,它會在管道中用到,管道中 labeltrace 返回的偏函數應用中是固定的。因此:

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};

const traceAfterG = trace('after g');
複製代碼

...等同於:

const traceAfterG = value => {
  const label = 'after g';
  console.log(`${ label }: ${ value }`);
  return value;
};
複製代碼

若是咱們把 trace('after g') 換成 traceAfterG,就等同於下面:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};

// 柯里化版本的 trace() 能讓咱們避免這種代碼...
const traceAfterG = value => {
  const label = 'after g';
  console.log(`${ label }: ${ value }`);
  return value;
};

const g = n => n + 1;
const f = n => n * 2;

const h = pipe(
  g,
  traceAfterG,
  f,
  trace('after f'),
);

h(20);
複製代碼

總結

柯里化函數是一種把接受多參數的函數變爲接受單一參數的函數,經過使用第一個參數並返回使用餘下參數的一系列函數,直到全部的參數都被使用,而且函數應用結束,此時結果就會被返回。

偏函數應用是一種已經應用一些但非所有參數的函數。函數已經應用的參數被稱爲固定參數(Fixed Parameters)

無點風格是一種不須要引用參數的函數定義風格。通常來講,無點函數經過調用返回函數的函數來建立,例如柯里化函數。

柯里化函數對於函數組合很是有用,由於因爲函數組合的須要,你能夠把 n 元函數輕鬆地轉換成一元函數形式:管道內的函數必須是單一參數。

數據置後函數對於函數組合來講很是方便,由於它們能夠輕鬆地被用在無點風格中。

下一步

EricElliottJS.com 的會員能夠看到此話題的徹底指南視頻。會員能夠訪問 ES6 Curry & Composition 課程


Eric Elliott 是 Programming JavaScript Applications(O’Reilly) 的做者,而且是軟件導師制平臺 DevAnywhere.io 的合做創始人。他擁有爲 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN、BBC 和頂尖音樂藝術家包括 Usher、Frank Ocean、Metallica 等工做的經驗。

他有着世界上最漂亮的女人陪着他在世界各地遠程工做。

感謝 JS_Cheerleader

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索