原文: Eric Elliott - Curry and Function Compositionjavascript
譯文: curry和函數組合java
提醒: 本文略長,慎重閱讀git
以前看到有文章說柯里化函數,大體看了下,就是高階函數,只是名字聽起來比較高大上一點,今天逛medium
又發現了這個,看了下感受還不錯,有涉及到閉包,涉及到point-free style
, 並非一股腦的安利demo
了事,這個得記錄下。github
curried
函數curried
函數是個一次一個的去獲取多個參數的函數。 再明白點,就是好比 給定一個帶有3個參數的函數,curried
版的函數將接受一個參數並返回一個接受下一個參數的函數,該函數返回一個接受第三個參數的函數。最後一個函數返回將函數應用於其全部參數的結果。編程
看下面的例子,例如,給定兩個數字,a
和b
爲curried
形式,返回a
和b
的總和:數組
// add = a => b => Number
const add = a => b => a + b;
複製代碼
而後就能夠直接調用了:閉包
const result = add(2)(3); // => 5
複製代碼
首先,函數接受a
做爲參數,而後返回一個新的函數,而後將b
傳遞給這個新的函數,最後返回a
和b
的和。每次只傳遞一個參數。若是函數有更多參數,不止上面的a,b
兩個參數,它能夠繼續像上面這樣返回新的函數,直到最後結束這個函數。app
add
函數接受一個參數以後,而後在這個閉包的範圍內返回固定的一部分功能。閉包就是與詞法範圍捆綁在一塊兒的函數。在運行函數建立時建立了閉包,能夠在這裏瞭解更多。固定
的意思是說變量在閉包的綁定範圍內賦值。函數
在來看看上面的代碼: add
用參數2
去調用,返回一個部分應用的函數,而且固定a
爲2
。咱們不是將返回值賦值給變量或以其餘方式使用它,而是經過在括號中將3
傳遞給它來當即調用返回的函數,從而完成整個函數並返回5
。ui
部分應用程序( partial application )是一個已應用於某些但並非所有參數的函數。直白的來講就是一個在閉包範圍內固定了(不變)的一些參數的函數。具備一些參數被固定的功能被認爲是部分應用的。
部分功能(partial application)能夠根據須要一次使用多個或幾個參數。 柯里化函數(curried function)每次返回一個一元函數: 每次攜帶一個參數的函數。
全部curried
函數都返回部分應用程序,但並不是全部部分應用程序都是curried
函數的結果。
對於curried
來講,一元函數的這個要求是一個重要的特徵。
point-free
風格point-free
是一種編程風格,其中函數定義不引用函數的參數。
咱們先來看看js
中函數的定義:
function foo (/* parameters are declared here*/) {
// ...
}
const foo = (/* parameters are declared here */) => // ...
const foo = function (/* parameters are declared here */) {
// ...
}
複製代碼
如何在不引用所需參數的狀況下在JavaScript
中定義函數?好吧,咱們不能使用function
這個關鍵字,咱們也不能使用箭頭函數(=>
),由於它們須要聲明形式參數(引用它的參數)。因此咱們須要作的就是 調用一個返回函數的函數。
建立一個函數,使用point-free
增長傳遞給它的任何數字。記住,咱們已經有一個名爲add
的函數,它接受一個數字並返回一個部分應用(partial application)的函數,其第一個參數固定爲你傳入的任何內容。咱們能夠用它來建立一個名爲inc()
的新函數:
/ inc = n => Number
// Adds 1 to any 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
,而且應用程序完成,返回1
和3
之和。
全部curried
函數都是高階函數的一種形式,它容許你爲手頭的特定用例建立原始函數的專用版本。
curry
curried
函數在函數組合的上下文中特別有用。
在代數中,給出了兩個函數f
和g
:
f: a -> b
g: b -> c
複製代碼
咱們能夠將這些函數組合在一塊兒建立一個新的函數(h
),h
從a
直接到c
:
//代數定義,借用`.`組合運算符
//來自Haskell
h: a -> c
h = f . g = f(g(x))
複製代碼
在js
中:
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);
複製代碼
此版本接受任意數量的函數並返回一個取初始值x
的函數,而後使用reduceRight()
在fns
中從右到左迭代每一個函數f
,而後將其依次應用於累積值y
。咱們在累加器中積累的函數,y
在此函數中是由compose()
返回的函數的返回值。
如今咱們能夠像這樣編寫咱們的組合:
const g = n => n + 1;
const f = n => n * 2;
// replace `x => f(g(x))` with `compose(f, g)`
const h = compose(f, g);
h(20); //=> 42
複製代碼
trace
)使用point-free
風格的函數組合建立了很是簡潔,可讀的代碼,可是他不易於調試。若是要檢查函數之間的值,該怎麼辦? trace()
是一個方便實用的函數,可讓你作到這一點。它採用curried
函數的形式:
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;
/* Note: function application order is bottom-to-top: */
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);
複製代碼
如今咱們能夠用pipe
把上面的重寫下:
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;
/* Now the function application order runs top-to-bottom: */
const h = pipe(
g,
trace('after g'),
f,
trace('after f'),
);
h(20);
/* after g: 21 after f: 42 */
複製代碼
即便在函數組合的上下文以外,curry
無疑是一個有用的抽象,能夠來作一些特定的事情。例如,一個curried
版本的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]
複製代碼
可是,curried
函數的真正強大之處在於它們簡化了函數組合。函數能夠接受任意數量的輸入,但只能返回單個輸出。爲了使函數可組合,輸出類型必須與預期的輸入類型對齊:
f: a => b
g: b => c
h: a => c
複製代碼
若是上面的g
函數預期有兩個參數,則f
的輸出不會與g
的輸入對齊:
f: a => b
g: (x, b) => c
h: a => c
複製代碼
在這種狀況下咱們如何得到x
?一般,答案是curry g
。
記住curried
函數的定義是一個函數,它經過獲取第一個參數並返回一系列的函數一次獲取一個參數而且每一個參數都採用下一個參數,直到收集完全部參數。
這個定義中的關鍵詞 是「一次一個」。curry
函數對函數組合如此方便的緣由是它們將指望多個參數的函數轉換爲能夠採用單個參數的函數,容許它們適合函數組合管道。以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
不是curry
,咱們就不能以這種方式使用它。咱們必須像這樣編寫管道:
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`調用再也不是`point-free`風格,
// 引入中間變量, `x`.
x => trace('after g', x),
f,
x => trace('after f', x),
);
h(20);
複製代碼
可是簡單的curry
函數是不夠的,還須要確保函數按正確的參數順序來專門化它們。看看若是咱們再次curry 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,
// the trace() calls can't be point-free,
// because arguments are expected in the wrong order.
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);
複製代碼
再這樣使用這個flippedTrace
:
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
的每一個應用程序都建立了一個在管道中使用的trace
函數的專用版本,其中label
被固定在返回的trace
部分應用程序中。因此這:
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;
};
複製代碼
若是咱們爲traceAfterG
交換trace('after g')
,那就意味着一樣的事情:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
// The curried version of trace()
// saves us from writing all this code...
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);
複製代碼
curried
函數是一個函數,經過取第一個參數,一次一個地獲取多個參數,並返回一系列函數,每一個函數接受下一個參數,直到全部參數都已修復,而且函數應用程序能夠完成,此時返回結果值。
部分應用程序( partial application )是一個已經應用於某些 - 但還沒有所有參數參與的函數。函數已經應用的參數稱爲固定參數。
point-free style
是一種定義函數而不引用其參數的方法。一般,經過調用返回函數的函數(例如curried
函數)來建立point-free
函數。
curried
函數很是適合函數組合 ,由於它們容許你輕鬆地將n元
函數轉換爲函數組合管道所需的一元函數形式:管道中的函數必須只接收一個參數。
數據最後 的功能便於功能組合,由於它們能夠輕鬆地用於point-free style
。