函数式编程:柯里化

柯里化(Currying)是函数式编程一个不可或缺的技术。柯里化就像手机电脑一样,当你没有的时候,你也许觉得它无足轻重;可一旦你拥有了,就会发现再也离不开它了。那么,什么是柯里化?它会用来解决什么问题?为什么说它是一个不可或缺的技术?

什么是柯里化

所谓柯里化,是一种将多参数函数的赋值操作转化成一系列函数的赋值操作的技术。

这样下定义可能有些抽象,我们先通过一个例子来说明。

function add(x, y) {
    return x + y;
}

这是一个再普通不过的包含两个参数的函数了。那么,柯里化是如何将这种多参数的函数的赋值操作转化为一系列函数的赋值操作呢?我们来看这样的修改

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

这种修改和原始代码的区别在于,我们把原来两个参数的 add 函数变成了单个参数的函数,同时,add 函数的返回结果是另一个等待传递参数的函数,也就是说变成了两个函数序列,这就是我们的柯里化技术。我们再来看如何使用这个柯里化后的函数。

var increment = add(1); // => function(y) { return 1 + y; }
var addFive = add(5); // => function(y) { return 5 + y; }

increment(3); // => 4
addFive(2); // => 7

我们再来看看三个参数的情形。还是以 add 函数为例。

function add(x, y, z) {
    return x + y + z;
}

柯里化后的结果为

function add(x) {
    return function(y) {
        return function(z) {
            return x + y + z;
        };
    };
}

由上面的两个例子可知,柯里化一个多参数函数本身并不难。实际上,JavaScript 的 lodash 库提供的 currycurryRight 方法,可以很方便地帮助我们创建柯里化函数。具体用法如下:

var _ = require('lodash');

var abc = function(a, b, c) {
    return [a, b, c];
};

var curried = _.curry(abc);

curried(1)(2)(3);
// => [1, 2, 3]

curried(1, 2)(3);
// => [1, 2, 3]

curried(1, 2, 3);
// => [1, 2, 3]

// Curried with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]
var _ = require('lodash');

var abc = function(a, b, c) {
    return [a, b, c];
};

var curried = _.curryRight(abc);

curried(3)(2)(1);
// => [1, 2, 3]

curried(2, 3)(1);
// => [1, 2, 3]

curried(1, 2, 3);
// => [1, 2, 3]

// Curried with placeholders.
curried(3)(1, _)(2);
// => [1, 2, 3]

柯里化的优势

那么,为什么我们要对多参数函数做柯里化操作?我们先用几个例子来看看柯里化的强大。

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, arr) {
    return arr.filter(f);
});

var map = curry(function(f, arr) {
    return arr.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, ['hello world', 'spaceless']);
// => ['hello world']

/*******************************/

var findSpaces = filter(hasSpaces);
// => function(xs) { return xs.filter(function(x) { return x.match(/\s+/g); }); }

findSpaces(['hello world', 'spaceless']);
// => ['hello world']

我们看到了,通过柯里化,把参数传递转化为一个记住了传递给它参数的新函数,使我们可以使用这个函数方便地进行后续的操作:在多个地方直接使用,或者作为参数再传递给另一个函数(同样这个函数也可以做柯里化处理)。

柯里化让我们可以更方便地得到一个新的处理函数。

当然,这还不是柯里化的全部。继续看下面的代码

var objects = [
    {id: 1, name: "one"},
    {id: 2, name: "two"},
    {id: 3, name: "three"}
];

objects.map(function(o) { return o.id; });
// => [1, 2, 3]

objects.map(function(o) { return o.name; });
// => ["one", "two", "three"]

这段代码看起来足够简单,但实际上并不符合我们的逻辑习惯。如果改成下面这样会怎样?

var get = curry(function(property, object) {
    return object[property];
});

objects.map(get("id"));
// => [1, 2, 3]

objects.map(get("name"));
// => ["one", "two", "three"]

可以看到,通过柯里化,一方面我们很方便地得到了两个处理函数,另一方面,这种写法也更符合我们的逻辑习惯:映射出 objects 中每一个实例的 idname

柯里化让我们的代码读起来更接近逻辑推导。

另外,更为重要的是,

柯里化让我们的代码更加简洁。

这一点,上面的例子已经说明了问题了。我们还可以通过下面的例子进一步说明。假设我们要通过网络请求获得如下数据:

{
    "user": "pyericz",
    "posts": [
        { "title": "函数式编程:柯里化", "contents": "..." },
        { "title": "函数式编程:纯函数", "contents": "..." }
    ]
}

传统的做法一般如下:

fetchFromServer()
    .then(JSON.parse)
    .then(function(data){ return data.posts })
    .then(function(posts){
        return posts.map(function(post){ return post.title })
    });

通过定义如下柯里化函数

var get = curry(function(property, object) {
    return object[property];
});

var map = curry(function(f, arr) {
    return arr.map(f);
});

可以把上面的请求处理变成

fetchFromServer()
    .then(JSON.parse)
    .then(get('posts'))
    .then(map(get('title')));

通过柯里化,我们的对 promise 的处理变得简洁许多。

留下评论