javascript笔记之函数柯里化

介绍部分来自JS函数式编程指南

介绍

柯里化(curry)的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。回忆下之前讲作用域时提到的闭包概念,这里的一部分参数的缓存就是通过闭包实现的,仔细阅读下面的代码,你会发现无处不在的闭包。

curry函数可以一次性调用,也可以分多次调用,使用方式如下:

1
2
3
4
5
6
7
8
9
var add = function(x){
return function(y){
return x+y;
}
}
var increment = add(1);
var addTen = increment(10);
increment(2); // 3
addTen(2); // 3

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)放到最后一个参数里。到使用它们的时候你就明白这样做的原因是什么了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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'

这里表明的是一种“预加载”函数的能力(有些地方也把这个称作延迟计算的能力),通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。这是闭包的应用

curry 的用处非常广泛,就像在 hasSpaces、findSpaces 和 censored 看到的那样,只需传给函数一些参数,就能得到一个新函数。只传给函数一部分参数通常也叫做局部调用(partial application),能够大量减少样板文件代码(boilerplate code)。

当我们谈论纯函数的时候,我们说它们接受一个输入返回一个输出。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出啊。哪怕输出是另一个函数,它也是纯函数。当然 curry 函数也允许一次传递多个参数,但这只是出于减少 () 的方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 曾经使用模板的时候写过这样的代码
function handleBar(template, data){
return template.replace('{{name}}', data.name);
}
handleBar("<div>我的名字{{name}}</div>",{name:"dhx"});
handleBar("<div>{{name}}发表xx</div>",{name:"xingmu"});
// 使用柯里化之后,代码就清晰多了
function handleBar(template){
return function(data){
return template.replace('{{name}}', data.name);
}
}
var nameRender = handleBar("<div>我的名字{{name}}</div>");
var nameRender2 = handleBar("<div>{{name}}发表xx</div>");
nameRender({name:"dhx"});
nameRender2({name:"xingmu"});

封装一个curry函数的函数

通过上一节的介绍我们知道,函数柯里化的过程,其实就是一个收集参数的过程,我们将每一次传入的参数都收集起来,并在完成参数的收集后,完成计算处理,我们可以尝试借助这个思路来封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 简单实现,参数只能从右到左传递
function curry(fn) {
var argus= Array.prototype.slice.call(arguments,1);
return function(){
var _argus = argus.concat([...arguments]);
if(_argus.length >= fn.length){
// 参数收集完毕,执行
return fn.apply(null,_argus);
}
// 如果参数个数小于最初的func.length,则递归调用,继续收集参数
return curry.apply(null,[fn,..._argus]);
}
}
// test
var abc = function (a, b, c) {
console.log([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]

柯里化与偏函数

当把已知函数的一些参数固定,结果函数被称为偏函数,通过使用bind获得偏函数,也有其他方式实现。当我们不想一次一次重复相同的参数时,偏函数是很便捷的。如我们有send(from,to)函数,如果from总是相同的,可以使用偏函数简化调用。

而柯里化是用来转换函数调用从f(a,b,c)至f(a)(b)(c)的一种思路,也可通过柯里化的方式来实现偏函数。

总结

curry 函数用起来非常得心应手,堪称手头必备工具,能够让函数式编程不那么繁琐和沉闷。