函数式编程指南中文版
  • README
  • 第 1 章: 我们在做什么?
    • 介绍
    • 一个简单例子
  • 第 2 章: 一等公民的函数
    • 快速概览
    • 为何钟爱一等公民
  • 第 3 章: 纯函数的好处
    • 再次强调“纯”
    • 副作用可能包括...
    • 八年级数学
    • 追求“纯”的理由
    • 总结
  • 第 4 章: 柯里化(curry)
    • 不可或缺的 curry
    • 不仅仅是双关语/咖喱
    • 总结
  • 第 5 章: 代码组合(compose)
    • 函数饲养
    • pointfree
    • debug
    • 范畴学
    • 总结
  • 第 6章: 示例应用
    • 声明式代码
    • 一个函数式的 flickr
    • 有原则的重构
    • 总结
  • 第 7 章: Hindley-Milner 类型签名
    • 初识类型
    • 神秘的传奇故事
    • 缩小可能性范围
    • 自由定理
    • 总结
  • 第 8 章: 特百惠
    • 强大的容器
    • 第一个 functor
    • 薛定谔的 Maybe
    • “纯”错误处理
    • 王老先生有作用...
    • 异步任务
    • 一点理论
    • 总结
  • 第 9 章: Monad
    • pointed functor
    • 混合比喻
    • chain 函数
    • 理论
    • 总结
  • 第 10 章: Applicative Functor
    • 应用 applicative functor
    • 瓶中之船
    • 协调与激励
    • lift
    • 免费开瓶器
    • 定律
    • 总结
  • 第 11 章: 再转换一次,就很自然
    • 令人生厌的嵌套
    • 一场情景喜剧
    • 全都很自然
    • 有原则的类型转换
    • 方法狂
    • 同构的 JavaScript
    • 更加宽泛的定义
    • 实现单层嵌套的方法
    • 总结
  • 第 12 章: 遍历
    • 类型与类型
    • 类型风水
    • 作用组合
    • 类型的华尔兹
    • 定律
    • 同一律
    • 组合
    • 自然
    • 总结
Powered by GitBook
On this page
  • 不可或缺的 curry
  • 不仅仅是双关语/咖喱
  • 总结
  • 练习

Was this helpful?

第 4 章: 柯里化(curry)

不可或缺的 curry

(译者注:原标题是“Can't live if livin' is without you”,为英国乐队 Badfinger 歌曲 Without You 中歌词。)

我父亲以前跟我说过,有些事物在你得到之前是无足轻重的,得到之后就不可或缺了。微波炉是这样,智能手机是这样,互联网也是这样——老人们在没有互联网的时候过得也很充实。对我来说,函数的柯里化(curry)也是这样。

curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

你可以一次性地调用 curry 函数,也可以每次只传一个参数分多次调用。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

这里我们定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。一次性地调用它实在是有点繁琐,好在我们可以使用一个特殊的 curry 帮助函数(helper function)使这类函数的定义和调用更加容易。

我们来创建一些 curry 函数享受下(译者注:此处原文是“for our enjoyment”,语出自圣经)。

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);
});

我在上面的代码中遵循的是一种简单,同时也非常重要的模式。即策略性地把要操作的数据(String, Array)放到最后一个参数里。到使用它们的时候你就明白这样做的原因是什么了。

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'

这里表明的是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。

我鼓励你使用 npm install lodash 安装 lodash,复制上面的代码放到 REPL 里跑一跑。当然你也可以在能够使用 lodash 或 ramda 的网页中运行它们。

不仅仅是双关语/咖喱

curry 的用处非常广泛,就像在 hasSpaces、findSpaces 和 censored 看到的那样,只需传给函数一些参数,就能得到一个新函数。

用 map 简单地把参数是单个元素的函数包裹一下,就能把它转换成参数为数组的函数。

var getChildren = function(x) {
  return x.childNodes;
};

var allTheChildren = map(getChildren);

只传给函数一部分参数通常也叫做局部调用(partial application),能够大量减少样板文件代码(boilerplate code)。考虑上面的 allTheChildren 函数,如果用 lodash 的普通 map 来写会是什么样的(注意参数的顺序也变了):

var allTheChildren = function(elements) {
  return _.map(elements, getChildren);
};

通常我们不定义直接操作数组的函数,因为只需内联调用 map(getChildren) 就能达到目的。这一点同样适用于 sort、filter 以及其他的高阶函数(higher order function)(高阶函数:参数或返回值为函数的函数)。

当我们谈论纯函数的时候,我们说它们接受一个输入返回一个输出。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出啊。

哪怕输出是另一个函数,它也是纯函数。当然 curry 函数也允许一次传递多个参数,但这只是出于减少 () 的方便。

总结

curry 函数用起来非常得心应手,每天使用它对我来说简直就是一种享受。它堪称手头必备工具,能够让函数式编程不那么繁琐和沉闷。

通过简单地传递几个参数,就能动态创建实用的新函数;而且还能带来一个额外好处,那就是保留了数学的函数定义,尽管参数不止一个。 下一章我们将学习另一个重要的工具:组合(compose)。

练习

var _ = require('ramda');


// 练习 1
//==============
// 通过局部调用(partial apply)移除所有参数

var words = function(str) {
  return split(' ', str);
};

// 练习 1a
//==============
// 使用 `map` 创建一个新的 `words` 函数,使之能够操作字符串数组

var sentences = undefined;


// 练习 2
//==============
// 通过局部调用(partial apply)移除所有参数

var filterQs = function(xs) {
  return filter(function(x){ return match(/q/i, x);  }, xs);
};


// 练习 3
//==============
// 使用帮助函数 `_keepHighest` 重构 `max` 使之成为 curry 函数

// 无须改动:
var _keepHighest = function(x,y){ return x >= y ? x : y; };

// 重构这段代码:
var max = function(xs) {
  return reduce(function(acc, x){
    return _keepHighest(acc, x);
  }, -Infinity, xs);
};


// 彩蛋 1:
// ============
// 包裹数组的 `slice` 函数使之成为 curry 函数
// //[1,2,3].slice(0, 2)
var slice = undefined;


// 彩蛋 2:
// ============
// 借助 `slice` 定义一个 `take` curry 函数,该函数调用后可以取出字符串的前 n 个字符。
var take = undefined;
Previous第 3 章: 纯函数的好处Next第 5 章: 代码组合(compose)

Last updated 2 years ago

Was this helpful?

开始练习之前先说明一下,我们将默认使用 这个库来把函数转为 curry 函数。或者你也可以选择由 lodash 的作者编写和维护的 。这两个库都很好用,选择哪一个就看你自己的喜好了。

你还可以对自己的练习代码做,或者把代码拷贝到一个 REPL 里运行看看。

这些练习的答案可以在中找到。

第 5 章: 代码组合(compose)
ramda
lodash-fp
单元测试
本书仓库