第04章 柯里化
若失去你 生命将告别完整
父亲曾说过,有些东西在获得前看似可有可无。微波炉就是其一,智能手机亦如是。许多年长读者还记得没有互联网的充实生活。对我而言,柯里化也在其列。
其核心理念很简单:当调用函数时传递的参数少于预期,该函数会返回接收剩余参数的新函数。
可以选择一次性传递全部参数,或分步逐个注入参数。
const add = x => y => x + y;
const increment = add(1);
const addTen = add(10);
increment(2); // 3
addTen(2); // 12这里我们创建了接收单一参数并返回新函数的add函数。通过调用过程,返回的新函数通过闭包持续记住首个参数。不过一次性传递两个参数稍显笨拙,因此我们可以使用curry辅助函数来简化这类函数的定义与调用。
下面定义几个柯里化函数以供实践。我们将统一使用附录A - 核心函数支持中定义的curry函数。
const match = curry((what, s) => s.match(what));
const replace = curry((what, replacement, s) => s.replace(what, replacement));
const filter = curry((f, xs) => xs.filter(f));
const map = curry((f, xs) => xs.map(f));当前模式虽简单却至关重要:我们将操作对象(字符串、数组)策略性放置于参数列表末尾,具体缘由会在使用时明朗。
(正则表达式/r/g表示匹配所有字母'r',更多信息请参阅正则表达式指南)
match(/r/g, 'hello world'); // [ 'r' ]
const hasLetterR = match(/r/g); // x => x.match(/r/g)
hasLetterR('hello world'); // [ 'r' ]
hasLetterR('just j and s and t etc'); // null
filter(hasLetterR, ['rock and roll', 'smooth jazz']); // ['rock and roll']
const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(x => x.match(/r/g))
removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']); // ['rock and roll', 'drum circle']
const noVowels = replace(/[aeiou]/ig); // (r,x) => x.replace(/[aeiou]/ig, r)
const censored = noVowels('*'); // x => x.replace(/[aeiou]/ig, '*')
censored('Chocolate Rain'); // 'Ch*c*l*t* R**n'这展示了'预加载'参数的能力——通过传递部分参数获取能记住这些参数的新函数。
建议克隆Mostly Adequate代码库(git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git),复制代码并在REPL中实践。附录中定义的curry等工具可通过support/index.js模块调用。
或通过npm查看已发布版本:
npm install @mostly-adequate/support绝非文字游戏 / 秘制酱汁
柯里化用途广泛。如hasLetterR、removeStringsWithoutRs和censored示例所示,对基础函数传递部分参数即可生成新函数。
通过map包装,可将处理单个元素的函数转换为处理数组的函数:
const getChildren = x => x.childNodes;
const allTheChildren = map(getChildren);传递少于预期参数被称为部分应用。该技术能大幅减少模板代码。试想用lodash非柯里化map(参数顺序不同)实现allTheChildren函数的情形:
const allTheChildren = elements => map(elements, getChildren);通常不必特地为数组操作定义函数,因为可以直接内联调用map(getChildren)。同样适用于sort、filter等高阶函数(高阶函数指接收或返回函数的函数)。
谈及纯函数的1输入对应1输出原则时,柯里化完美契合:每个参数输入都会返回处理剩余参数的新函数。这正是经典的1对1输入输出模型。
即使输出是函数也满足纯度要求。允许一次性传递多个参数仅是为省略括号的便利写法。
本章小结
柯里化兼具实用与优雅,是日常开发的利器,能显著降低函数式编程的繁琐性。
通过部分参数即时创建新函数,既能保持数学函数的严谨定义,又提升了开发灵活性。
接下来我们将学习另一个关键工具——compose。
练习题
练习说明
全书各章节均设有练习板块。推荐通过gitbook在线阅读以在浏览器中直接完成练习。
注意:所有练习中,附录A、B、C定义的工具函数均预置在全局作用域。部分练习提供的专用辅助函数也可直接调用。
提示:在编辑器中按 Ctrl + Enter 提交解答。
本地运行练习(可选)
若要在本地编辑器中完成练习:
- 克隆仓库(
git clone git@github.com:MostlyAdequate/mostly-adequate-guide.git) - 进入练习目录(
cd mostly-adequate-guide/exercises) - 确保使用node v10.22.1版本(如
nvm install),详见项目README - 通过npm安装依赖(
npm install) - 修改对应章节的*exercise_**文件完成练习
- 运行测试命令(如
npm run ch04)
单元测试将验证答案并提供错误提示。练习答案参见*solution_**文件。
开始实践!
开始练习!
通过部分应用重构函数,移除所有参数。 const words = str => split(' ', str);
开始练习!
通过部分应用函数进行重构,移除所有参数。 const filterQs = xs => filter(x => match(/q/i, x), xs);
已知如下函数:
const keepHighest = (x, y) => (x >= y ? x : y);开始练习!
使用辅助函数keepHighest重构max函数,移除所有参数引用。
const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs);