原文: Eric Elliott - Curry and Function Composition
譯文: curry和函數(shù)組合
提醒: 本文略長(zhǎng),慎重閱讀
之前看到有文章說(shuō)柯里化函數(shù),大致看了下,就是高階函數(shù),只是名字聽(tīng)起來(lái)比較高大上一點(diǎn)缎玫,今天逛medium
又發(fā)現(xiàn)了這個(gè)取试,看了下感覺(jué)還不錯(cuò),有涉及到閉包涝开,涉及到point-free style
, 并不是一股腦的安利demo
了事,這個(gè)得記錄下框仔。
什么是curried
函數(shù)
curried
函數(shù)是個(gè)一次一個(gè)的去獲取多個(gè)參數(shù)的函數(shù)舀武。 再明白點(diǎn),就是比如 給定一個(gè)帶有3個(gè)參數(shù)的函數(shù)离斩,curried
版的函數(shù)將接受一個(gè)參數(shù)并返回一個(gè)接受下一個(gè)參數(shù)的函數(shù)银舱,該函數(shù)返回一個(gè)接受第三個(gè)參數(shù)的函數(shù)。最后一個(gè)函數(shù)返回將函數(shù)應(yīng)用于其所有參數(shù)的結(jié)果跛梗。
看下面的例子寻馏,例如,給定兩個(gè)數(shù)字核偿,a
和b
為curried
形式诚欠,返回a
和b
的總和:
// add = a => b => Number
const add = a => b => a + b;
然后就可以直接調(diào)用了:
const result = add(2)(3); // => 5
首先,函數(shù)接受a
作為參數(shù)漾岳,然后返回一個(gè)新的函數(shù)轰绵,然后將b
傳遞給這個(gè)新的函數(shù),最后返回a
和b
的和尼荆。每次只傳遞一個(gè)參數(shù)左腔。如果函數(shù)有更多參數(shù),不止上面的a,b
兩個(gè)參數(shù)捅儒,它可以繼續(xù)像上面這樣返回新的函數(shù)液样,直到最后結(jié)束這個(gè)函數(shù)振亮。
add
函數(shù)接受一個(gè)參數(shù)之后,然后在這個(gè)閉包的范圍內(nèi)返回固定的一部分功能鞭莽。閉包就是與詞法范圍捆綁在一起的函數(shù)双炕。在運(yùn)行函數(shù)創(chuàng)建時(shí)創(chuàng)建了閉包,可以在這里了解更多撮抓。固定
的意思是說(shuō)變量在閉包的綁定范圍內(nèi)賦值妇斤。
在來(lái)看看上面的代碼: add
用參數(shù)2
去調(diào)用,返回一個(gè)部分應(yīng)用的函數(shù)丹拯,并且固定a
為2
站超。我們不是將返回值賦值給變量或以其他方式使用它,而是通過(guò)在括號(hào)中將3
傳遞給它來(lái)立即調(diào)用返回的函數(shù)乖酬,從而完成整個(gè)函數(shù)并返回5
死相。
什么是部分功能
部分應(yīng)用程序( partial application )是一個(gè)已應(yīng)用于某些但并不是全部參數(shù)的函數(shù)。直白的來(lái)說(shuō)就是一個(gè)在閉包范圍內(nèi)固定了(不變)的一些參數(shù)的函數(shù)咬像。具有一些參數(shù)被固定的功能被認(rèn)為是部分應(yīng)用的算撮。
有什么不同?
部分功能(partial application)可以根據(jù)需要一次使用多個(gè)或幾個(gè)參數(shù)县昂。
柯里化函數(shù)(curried function)每次返回一個(gè)一元函數(shù): 每次攜帶一個(gè)參數(shù)的函數(shù)肮柜。
所有curried
函數(shù)都返回部分應(yīng)用程序,但并非所有部分應(yīng)用程序都是curried
函數(shù)的結(jié)果倒彰。
對(duì)于curried
來(lái)說(shuō)审洞,一元函數(shù)的這個(gè)要求是一個(gè)重要的特征。
什么是point-free
風(fēng)格
point-free
是一種編程風(fēng)格待讳,其中函數(shù)定義不引用函數(shù)的參數(shù)芒澜。
我們先來(lái)看看js
中函數(shù)的定義:
function foo (/* parameters are declared here*/) {
// ...
}
const foo = (/* parameters are declared here */) => // ...
const foo = function (/* parameters are declared here */) {
// ...
}
如何在不引用所需參數(shù)的情況下在JavaScript
中定義函數(shù)?好吧创淡,我們不能使用function
這個(gè)關(guān)鍵字痴晦,我們也不能使用箭頭函數(shù)(=>
),因?yàn)樗鼈冃枰暶餍问絽?shù)(引用它的參數(shù))琳彩。所以我們需要做的就是 調(diào)用一個(gè)返回函數(shù)的函數(shù)誊酌。
創(chuàng)建一個(gè)函數(shù),使用point-free
增加傳遞給它的任何數(shù)字汁针。記住术辐,我們已經(jīng)有一個(gè)名為add
的函數(shù),它接受一個(gè)數(shù)字并返回一個(gè)部分應(yīng)用(partial application)的函數(shù)施无,其第一個(gè)參數(shù)固定為你傳入的任何內(nèi)容辉词。我們可以用它來(lái)創(chuàng)建一個(gè)名為inc()
的新函數(shù):
/ inc = n => Number
// Adds 1 to any number.
const inc = add(1);
inc(3); // => 4
這作為一種泛化和專業(yè)化的機(jī)制變得有趣。返回的函數(shù)只是更通用的add()
函數(shù)的專用版本猾骡。我們可以使用add()
創(chuàng)建任意數(shù)量的專用版本:
const inc10 = add(10);
const inc20 = add(20);
inc10(3); // => 13
inc20(3); // => 23
當(dāng)然瑞躺,這些都有自己的閉包范圍(閉包是在函數(shù)創(chuàng)建時(shí)創(chuàng)建的 - 當(dāng)調(diào)用add()
時(shí))敷搪,因此原來(lái)的inc()
繼續(xù)保持工作:
inc(3) // 4
當(dāng)我們使用函數(shù)調(diào)用add(1)
創(chuàng)建inc()
時(shí),add()
中的a
參數(shù)在返回的函數(shù)內(nèi)被固定為1
幢哨,該函數(shù)被賦值給inc
赡勘。
然后當(dāng)我們調(diào)用inc(3)
時(shí),add()
中的b
參數(shù)被替換為參數(shù)值3
捞镰,并且應(yīng)用程序完成闸与,返回1
和3
之和。
所有curried
函數(shù)都是高階函數(shù)的一種形式岸售,它允許你為手頭的特定用例創(chuàng)建原始函數(shù)的專用版本践樱。
為什么要curry
curried
函數(shù)在函數(shù)組合的上下文中特別有用。
在代數(shù)中凸丸,給出了兩個(gè)函數(shù)f
和g
:
f: a -> b
g: b -> c
我們可以將這些函數(shù)組合在一起創(chuàng)建一個(gè)新的函數(shù)(h
)拷邢,h
從a
直接到c
:
//代數(shù)定義,借用`.`組合運(yùn)算符
//來(lái)自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
代數(shù)的定義:
f . g = f(g(x))
可以翻譯成JavaScript
:
const compose = (f, g) => f(g(x));
但那只能一次組成兩個(gè)函數(shù)屎慢。在代數(shù)中瞭稼,可以寫(xiě):
g . f . h
我們可以編寫(xiě)一個(gè)函數(shù)來(lái)編寫(xiě)任意數(shù)量的函數(shù)。換句話說(shuō)腻惠,compose()
創(chuàng)建一個(gè)函數(shù)管道环肘,其中一個(gè)函數(shù)的輸出連接到下一個(gè)函數(shù)的輸入。
這是我經(jīng)常寫(xiě)的方法:
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
此版本接受任意數(shù)量的函數(shù)并返回一個(gè)取初始值x
的函數(shù)妖枚,然后使用reduceRight()
在fns
中從右到左迭代每個(gè)函數(shù)f
廷臼,然后將其依次應(yīng)用于累積值y
。我們?cè)诶奂悠髦蟹e累的函數(shù)绝页,y
在此函數(shù)中是由compose()
返回的函數(shù)的返回值。
現(xiàn)在我們可以像這樣編寫(xiě)我們的組合:
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
風(fēng)格的函數(shù)組合創(chuàng)建了非常簡(jiǎn)潔寂恬,可讀的代碼续誉,但是他不易于調(diào)試。如果要檢查函數(shù)之間的值初肉,該怎么辦酷鸦? trace()
是一個(gè)方便實(shí)用的函數(shù),可以讓你做到這一點(diǎn)牙咏。它采用curried
函數(shù)的形式:
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
現(xiàn)在我們可以使用這個(gè)來(lái)檢查函數(shù)了:
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()
是一個(gè)很棒的實(shí)用程序臼隔,但是當(dāng)我們需要編寫(xiě)兩個(gè)以上的函數(shù)時(shí),如果我們能夠按照從上到下的順序讀取它們妄壶,這有時(shí)會(huì)很方便摔握。我們可以通過(guò)反轉(zhuǎn)調(diào)用函數(shù)的順序來(lái)做到這一點(diǎn)。還有另一個(gè)名為pipe()
的組合實(shí)用程序丁寄,它以相反的順序組成:
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
現(xiàn)在我們可以用pipe
把上面的重寫(xiě)下:
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和功能組合一起
即使在函數(shù)組合的上下文之外氨淌,curry
無(wú)疑是一個(gè)有用的抽象泊愧,可以來(lái)做一些特定的事情。例如盛正,一個(gè)curried
版本的map()
可以專門(mén)用于做許多不同的事情:
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
函數(shù)的真正強(qiáng)大之處在于它們簡(jiǎn)化了函數(shù)組合。函數(shù)可以接受任意數(shù)量的輸入豪筝,但只能返回單個(gè)輸出痰滋。為了使函數(shù)可組合,輸出類型必須與預(yù)期的輸入類型對(duì)齊:
f: a => b
g: b => c
h: a => c
如果上面的g
函數(shù)預(yù)期有兩個(gè)參數(shù)续崖,則f
的輸出不會(huì)與g
的輸入對(duì)齊:
f: a => b
g: (x, b) => c
h: a => c
在這種情況下我們?nèi)绾潍@得x
敲街?通常,答案是curry g
袜刷。
記住curried
函數(shù)的定義是一個(gè)函數(shù)聪富,它通過(guò)獲取第一個(gè)參數(shù)并返回一系列的函數(shù)一次獲取一個(gè)參數(shù)并且每個(gè)參數(shù)都采用下一個(gè)參數(shù),直到收集完所有參數(shù)著蟹。
這個(gè)定義中的關(guān)鍵詞 是“一次一個(gè)”墩蔓。curry
函數(shù)對(duì)函數(shù)組合如此方便的原因是它們將期望多個(gè)參數(shù)的函數(shù)轉(zhuǎn)換為可以采用單個(gè)參數(shù)的函數(shù),允許它們適合函數(shù)組合管道萧豆。以trace()
函數(shù)為例奸披,從前面開(kāi)始:
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
定義了兩個(gè)參數(shù),但是一次只接受一個(gè)參數(shù)涮雷,允許我們專門(mén)化內(nèi)聯(lián)函數(shù)阵面。如果trace
不是curry
,我們就不能以這種方式使用它洪鸭。我們必須像這樣編寫(xiě)管道:
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`調(diào)用不再是`point-free`風(fēng)格样刷,
// 引入中間變量, `x`.
x => trace('after g', x),
f,
x => trace('after f', x),
);
h(20);
但是簡(jiǎn)單的curry
函數(shù)是不夠的,還需要確保函數(shù)按正確的參數(shù)順序來(lái)專門(mén)化它們览爵≈帽牵看看如果我們?cè)俅?code>curry trace()會(huì)發(fā)生什么,但是翻轉(zhuǎn)參數(shù)順序:
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()
的函數(shù)解決該問(wèn)題箕母,該函數(shù)只是翻轉(zhuǎn)兩個(gè)參數(shù)的順序:
const flip = fn => a => b => fn(b)(a);
現(xiàn)在我們可以創(chuàng)建一個(gè)flippedTrace
函數(shù):
const flippedTrace = flip(trace);
再這樣使用這個(gè)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);
可以發(fā)現(xiàn)這樣也可以工作,但是 首先就應(yīng)該以正確的方式去編寫(xiě)函數(shù)俱济。這個(gè)樣式有時(shí)稱為“數(shù)據(jù)最后”嘶是,這意味著你應(yīng)首先獲取特殊參數(shù),并獲取該函數(shù)最后作用的數(shù)據(jù)蛛碌。
看看這個(gè)函數(shù)的最初的形式:
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
trace
的每個(gè)應(yīng)用程序都創(chuàng)建了一個(gè)在管道中使用的trace
函數(shù)的專用版本聂喇,其中label
被固定在返回的trace
部分應(yīng)用程序中。所以這:
const trace = label => value => {
console.log(`${ label }: ${ value }`);
return value;
};
const traceAfterG = trace('after g');
等同于下面這個(gè):
const traceAfterG = value => {
const label = 'after g';
console.log(`${ label }: ${ value }`);
return value;
};
如果我們?yōu)?code>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);
總結(jié)
curried
函數(shù)是一個(gè)函數(shù)授帕,通過(guò)取第一個(gè)參數(shù)同木,一次一個(gè)地獲取多個(gè)參數(shù),并返回一系列函數(shù)跛十,每個(gè)函數(shù)接受下一個(gè)參數(shù)彤路,直到所有參數(shù)都已修復(fù),并且函數(shù)應(yīng)用程序可以完成芥映,此時(shí)返回結(jié)果值洲尊。
部分應(yīng)用程序( partial application )是一個(gè)已經(jīng)應(yīng)用于某些 - 但尚未全部參數(shù)參與的函數(shù)。函數(shù)已經(jīng)應(yīng)用的參數(shù)稱為固定參數(shù)奈偏。
point-free style
是一種定義函數(shù)而不引用其參數(shù)的方法坞嘀。通常,通過(guò)調(diào)用返回函數(shù)的函數(shù)(例如curried
函數(shù))來(lái)創(chuàng)建point-free
函數(shù)惊来。
curried
函數(shù)非常適合函數(shù)組合 丽涩,因?yàn)樗鼈冊(cè)试S你輕松地將n元
函數(shù)轉(zhuǎn)換為函數(shù)組合管道所需的一元函數(shù)形式:管道中的函數(shù)必須只接收一個(gè)參數(shù)。
數(shù)據(jù)最后 的功能便于功能組合裁蚁,因?yàn)樗鼈兛梢暂p松地用于point-free style
矢渊。