[TOC]
# 第 4 章: 柯里化(curry)
## 不可或缺的 curry
(譯者注:原標(biāo)題是“Can't live if livin' is without you”,為英國(guó)樂隊(duì) Badfinger 歌曲 *Without You* 中歌詞。)
我父親以前跟我說(shuō)過,有些事物在你得到之前是無(wú)足輕重的,得到之后就不可或缺了。微波爐是這樣,智能手機(jī)是這樣,互聯(lián)網(wǎng)也是這樣——老人們?cè)跊]有互聯(lián)網(wǎng)的時(shí)候過得也很充實(shí)。對(duì)我來(lái)說(shuō),函數(shù)的柯里化(curry)也是這樣。
curry 的概念很簡(jiǎn)單:只傳遞給函數(shù)一部分參數(shù)來(lái)調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)。
你可以一次性地調(diào)用 curry 函數(shù),也可以每次只傳一個(gè)參數(shù)分多次調(diào)用。
```js
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
```
這里我們定義了一個(gè) `add` 函數(shù),它接受一個(gè)參數(shù)并返回一個(gè)新的函數(shù)。調(diào)用 `add` 之后,返回的函數(shù)就通過閉包的方式記住了 `add` 的第一個(gè)參數(shù)。一次性地調(diào)用它實(shí)在是有點(diǎn)繁瑣,好在我們可以使用一個(gè)特殊的 `curry` 幫助函數(shù)(helper function)使這類函數(shù)的定義和調(diào)用更加容易。
我們來(lái)創(chuàng)建一些 curry 函數(shù)享受下(譯者注:此處原文是“for our enjoyment”,語(yǔ)出自圣經(jīng))。
```js
var curry = require('lodash').curry;
var match = curry(function(what, str) {
return str.match(what);
});
var replace = curry(function(what, replacement, str) {
return str.replace(what, replacement);
});
var filter = curry(function(f, ary) {
return ary.filter(f);
});
var map = curry(function(f, ary) {
return ary.map(f);
});
```
我在上面的代碼中遵循的是一種簡(jiǎn)單,同時(shí)也非常重要的模式。即策略性地把要操作的數(shù)據(jù)(String, Array)放到最后一個(gè)參數(shù)里。到使用它們的時(shí)候你就明白這樣做的原因是什么了。
```js
match(/\s+/g, "hello world");
// [ ' ' ]
match(/\s+/g)("hello world");
// [ ' ' ]
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
hasSpaces("hello world");
// [ ' ' ]
hasSpaces("spaceless");
// null
filter(hasSpaces, ["tori_spelling", "tori amos"]);
// ["tori amos"]
var findSpaces = filter(hasSpaces);
// function(xs) { return xs.filter(function(x) { return x.match(/\s+/g) }) }
findSpaces(["tori_spelling", "tori amos"]);
// ["tori amos"]
var noVowels = replace(/[aeiou]/ig);
// function(replacement, x) { return x.replace(/[aeiou]/ig, replacement) }
var censored = noVowels("*");
// function(x) { return x.replace(/[aeiou]/ig, "*") }
censored("Chocolate Rain");
// 'Ch*c*l*t* R**n'
```
這里表明的是一種“預(yù)加載”函數(shù)的能力,通過傳遞一到兩個(gè)參數(shù)調(diào)用函數(shù),就能得到一個(gè)記住了這些參數(shù)的新函數(shù)。
我鼓勵(lì)你使用 `npm install lodash` 安裝 `lodash`,復(fù)制上面的代碼放到 REPL 里跑一跑。當(dāng)然你也可以在能夠使用 `lodash` 或 `ramda` 的網(wǎng)頁(yè)中運(yùn)行它們。
## 不僅僅是雙關(guān)語(yǔ)/咖喱
curry 的用處非常廣泛,就像在 `hasSpaces`、`findSpaces` 和 `censored` 看到的那樣,只需傳給函數(shù)一些參數(shù),就能得到一個(gè)新函數(shù)。
用 `map` 簡(jiǎn)單地把參數(shù)是單個(gè)元素的函數(shù)包裹一下,就能把它轉(zhuǎn)換成參數(shù)為數(shù)組的函數(shù)。
```js
var getChildren = function(x) {
return x.childNodes;
};
var allTheChildren = map(getChildren);
```
只傳給函數(shù)一部分參數(shù)通常也叫做*局部調(diào)用*(partial application),能夠大量減少樣板文件代碼(boilerplate code)??紤]上面的 `allTheChildren` 函數(shù),如果用 lodash 的普通 `map` 來(lái)寫會(huì)是什么樣的(注意參數(shù)的順序也變了):
```js
var allTheChildren = function(elements) {
return _.map(elements, getChildren);
};
```
通常我們不定義直接操作數(shù)組的函數(shù),因?yàn)橹恍鑳?nèi)聯(lián)調(diào)用 `map(getChildren)` 就能達(dá)到目的。這一點(diǎn)同樣適用于 `sort`、`filter` 以及其他的高階函數(shù)(higher order function)(高階函數(shù):參數(shù)或返回值為函數(shù)的函數(shù))。
當(dāng)我們談?wù)?純函數(shù)*的時(shí)候,我們說(shuō)它們接受一個(gè)輸入返回一個(gè)輸出。curry 函數(shù)所做的正是這樣:每傳遞一個(gè)參數(shù)調(diào)用函數(shù),就返回一個(gè)新函數(shù)處理剩余的參數(shù)。這就是一個(gè)輸入對(duì)應(yīng)一個(gè)輸出啊。
哪怕輸出是另一個(gè)函數(shù),它也是純函數(shù)。當(dāng)然 curry 函數(shù)也允許一次傳遞多個(gè)參數(shù),但這只是出于減少 `()` 的方便。
## 總結(jié)
curry 函數(shù)用起來(lái)非常得心應(yīng)手,每天使用它對(duì)我來(lái)說(shuō)簡(jiǎn)直就是一種享受。它堪稱手頭必備工具,能夠讓函數(shù)式編程不那么繁瑣和沉悶。
通過簡(jiǎn)單地傳遞幾個(gè)參數(shù),就能動(dòng)態(tài)創(chuàng)建實(shí)用的新函數(shù);而且還能帶來(lái)一個(gè)額外好處,那就是保留了數(shù)學(xué)的函數(shù)定義,盡管參數(shù)不止一個(gè)。
下一章我們將學(xué)習(xí)另一個(gè)重要的工具:`組合`(compose)。
[第 5 章: 代碼組合(compose)](ch5.md)
## 練習(xí)
開始練習(xí)之前先說(shuō)明一下,我們將默認(rèn)使用 [ramda](http://ramdajs.com) 這個(gè)庫(kù)來(lái)把函數(shù)轉(zhuǎn)為 curry 函數(shù)?;蛘吣阋部梢赃x擇由 losash 的作者編寫和維護(hù)的 [lodash-fp](https://github.com/lodash/lodash-fp)。這兩個(gè)庫(kù)都很好用,選擇哪一個(gè)就看你自己的喜好了。
你還可以對(duì)自己的練習(xí)代碼做[單元測(cè)試](https://github.com/llh911001/mostly-adequate-guide-chinese/tree/master/code/part1_exercises),或者把代碼拷貝到一個(gè) REPL 里運(yùn)行看看。
這些練習(xí)的答案可以在[本書倉(cāng)庫(kù)](https://github.com/llh911001/mostly-adequate-guide-chinese/tree/master/code/part1_exercises/answers)中找到。
```js
var _ = require('ramda');
// 練習(xí) 1
//==============
// 通過局部調(diào)用(partial apply)移除所有參數(shù)
var words = function(str) {
return split(' ', str);
};
// 練習(xí) 1a
//==============
// 使用 `map` 創(chuàng)建一個(gè)新的 `words` 函數(shù),使之能夠操作字符串?dāng)?shù)組
var sentences = undefined;
// 練習(xí) 2
//==============
// 通過局部調(diào)用(partial apply)移除所有參數(shù)
var filterQs = function(xs) {
return filter(function(x){ return match(/q/i, x); }, xs);
};
// 練習(xí) 3
//==============
// 使用幫助函數(shù) `_keepHighest` 重構(gòu) `max` 使之成為 curry 函數(shù)
// 無(wú)須改動(dòng):
var _keepHighest = function(x,y){ return x >= y ? x : y; };
// 重構(gòu)這段代碼:
var max = function(xs) {
return reduce(function(acc, x){
return _keepHighest(acc, x);
}, -Infinity, xs);
};
// 彩蛋 1:
// ============
// 包裹數(shù)組的 `slice` 函數(shù)使之成為 curry 函數(shù)
// //[1,2,3].slice(0, 2)
var slice = undefined;
// 彩蛋 2:
// ============
// 借助 `slice` 定義一個(gè) `take` curry 函數(shù),該函數(shù)調(diào)用后可以取出字符串的前 n 個(gè)字符。
var take = undefined;
```
