js的柯里化函數(shù)(curry)

原文: 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ù)字核偿,abcurried形式诚欠,返回ab的總和:

// 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ù),最后返回ab的和尼荆。每次只傳遞一個(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ù)丹拯,并且固定a2站超。我們不是將返回值賦值給變量或以其他方式使用它,而是通過(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)用程序完成闸与,返回13之和。

所有curried函數(shù)都是高階函數(shù)的一種形式岸售,它允許你為手頭的特定用例創(chuàng)建原始函數(shù)的專用版本践樱。

為什么要curry

curried函數(shù)在函數(shù)組合的上下文中特別有用。

在代數(shù)中凸丸,給出了兩個(gè)函數(shù)fg

f: a -> b
g: b -> c

我們可以將這些函數(shù)組合在一起創(chuàng)建一個(gè)新的函數(shù)(h)拷邢,ha直接到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矢渊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市枉证,隨后出現(xiàn)的幾起案子矮男,更是在濱河造成了極大的恐慌,老刑警劉巖室谚,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毡鉴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡秒赤,警方通過(guò)查閱死者的電腦和手機(jī)猪瞬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)入篮,“玉大人撑螺,你說(shuō)我怎么就攤上這事∑槠” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵含潘,是天一觀的道長(zhǎng)饲做。 經(jīng)常有香客問(wèn)我,道長(zhǎng)遏弱,這世上最難降的妖魔是什么盆均? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮漱逸,結(jié)果婚禮上泪姨,老公的妹妹穿的比我還像新娘游沿。我一直安慰自己,他們只是感情好肮砾,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布诀黍。 她就那樣靜靜地躺著,像睡著了一般仗处。 火紅的嫁衣襯著肌膚如雪眯勾。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,821評(píng)論 1 290
  • 那天婆誓,我揣著相機(jī)與錄音吃环,去河邊找鬼。 笑死洋幻,一個(gè)胖子當(dāng)著我的面吹牛郁轻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播文留,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼好唯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了厂庇?” 一聲冷哼從身側(cè)響起渠啊,我...
    開(kāi)封第一講書(shū)人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎权旷,沒(méi)想到半個(gè)月后替蛉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拄氯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年躲查,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片译柏。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡镣煮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鄙麦,到底是詐尸還是另有隱情典唇,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布胯府,位于F島的核電站介衔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏骂因。R本人自食惡果不足惜炎咖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乘盼,春花似錦升熊、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至阴幌,卻和暖如春勺阐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背矛双。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工渊抽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人议忽。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓懒闷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親栈幸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子愤估,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容