函数式编程(Functional Programming) 简单说就是把函数当参数传递给其他函数。个人认为 FP 在软件抽象中占很重要的地位,作为程序员的话极力推荐掌握其中的思维方法。最早这思想出现在数学中, f(x) 中的x可以是变量也可以是函数,比如 f(f(y))。而当时计算机语言的函数还都只能接收变量参数而不接受函数参数,于是数学家发明了一种新的语言 scheme (Lisp的一个变种),到现在所有语言无不支持函数式编程。.net, java, python, php5.3+, c的函数指针,c++11还引入了lambda和functional,对js开发者来说更是重要,ext,jq等随处可见FP的影子。
FP 的最大特点是使代码更贴近于大脑思维流程,所以代码会更容易掌控。
1. 从一个例子开始:遍历一个数组,打印所有元素
// 方法1
for(var i=0; i<arr.length; i++) {
print(arr[i]);
}
// 方法2
function forEach(action, arr) {
for(var i=0; i<arr.length; i++)
action(arr[i], i);
}
forEach(print, arr);
先不考虑代码重用性,方法2的优点就是直观。forEach(print, arr) 对应 遍历(打印,数组) -- 大脑差不多就是这么想的。 这个例子可能过于简单,不能体现方法2的好处,后续碰到的问题越复杂越能体现FP的直观。
上面通过 forEach 把遍历这个行为封装起来,而把各种行为函数作为参数传递,通过修改各种行为来实现各种功能,比如用 forEach 实现累加(sum):
function sum(numbers) {
var total = 0;
forEach(function(number) {
total += number;
}, numbers);
return total;
}
print(sum([1, 10, 100])); //结果为 111
在看点复杂的,比如函数构造器:
function makeAddFunction(amount) {
function add(number) {
return number + amount;
}
return add;
}
var addTwo = makeAddFunction(2);
var addFive = makeAddFunction(5);
print( addTwo(1) + addFive(1) ); //结果为 9
和 函数适配器:
function negate(func) {
return function() {
return ! func.apply(null, arguments);
};
}
var isNotNaN = negate(isNaN);
print( isNotNaN(NaN) );//结果为 false
可以看到,不光参数可以是函数,返回值也一样可以,这种能操纵其他函数的函数叫作高阶函数。
2. 如果随便找一本关于FP的书,一定可以看到这两个函数 reduce & map,因为这两种数组操作实在太常见了。
reduce: 遍历数组,用combine函数把所有元素合并到一个值
function reduce(combine, base, array) {
forEach(array, function(element) {
base = combine(base, element);
});
return base;
}
之前的 sum 函数就是一个典型(把元素累加到一个值),所以能用 reduce 来改造 sum:
function add(a, b) {
return a + b;
}
function sum(numbers) {
return reduce(add, 0, numbers);
}
提问:写一个函数countZeroes,输入是个数字数组,返回出现0的次数
function countZeroes(numbers) {
function counter(total, element) {
return total + (element === 0 ? 1: 0);
}
return reduce(counter, 0, numbers);
}
写完后有没想到什么?yes,"数数"这个概念可以提取出来,成为高阶函数count:
function count(test, array) {
return reduce(function(total, element) {
return total + (test(element) ? 1 : 0);
}, 0, array);
}
function equals(x) {
return function(element) {
return element === x;
};
}
function countZeroes(numbers) {
return count(equals(0), numbers);
}
map: 遍历数组,把每一项都用函数func处理一遍,返回处理过的新数组
function map(func, array) {
var result = [];
forEach(array, function (element) {
result.push(func(element));
});
return result;
}
print( map(Math.round, [0.01, 2, 9.89, Math.PI]) );
//结果为 [0, 2, 10, 3]
怎么样?对FP有点不适应?反正我当时是花了点时间来适应这个,适应之后你看到什么都想把它搞成函数,拿加减乘除符号来说,就可以封装成函数-_-:
var op = {
"+": function(a, b){return a + b;},
"==": function(a, b){return a == b;},
"===": function(a, b){return a === b;},
"!": function(a){return !a;},
"<": function(a, b) {return a<b;},
">": function(a, b) {return a>b;}
};
这样一来,上面的 sum 函数又可以写成:
reduce(op["+"], 0, [1, 2, 3, 4, 5])
提问: 把数组[0, 2, 4, 6, 8, 10]的每一项都加1,生成新数组
最先想到的可能是这样:
arr = [0, 2, 4, 6, 8, 10];
function add1(x) {
return op['+'](1, x);
}
map(arr, add1);
问题是,add1只是每项加1,如果还有加2,加3, 那又要再写很多 add2, add3之类的辅助函数,其实区别就是 op['+'] 的第一个参数不同,而这个参数可以用偏函数来绑定。
3. 偏函数(Partial Function),用来绑定函数的一个或多个参数,c++里也叫bind
partial:
//注:javascript的arguments表示函数调用时的参数,是一个类似数组的东西,只是类似,并不是数组
// 所以不具备数组的一些功能,比如concat,slice之类。因此需要用下面的asArray将其转换为数组
function asArray(quasiArray, start) {
var result = [];
for (var i = (start || 0); i < quasiArray.length; i++)
result.push(quasiArray[i]);
return result;
}
function partial(func) {
var fixedArgs = asArray(arguments, 1);
return function() {
return func.apply(null, fixedArgs.concat(asArray(arguments)));
};
}
之前的 equals 函数如果用偏函数改造:
equals(10) <==> partial(op["=="], 10)
回到刚才的问题
//把每个元素加上1
print(map(partial(op["+"], 1), [0, 2, 4, 6, 8, 10])); //结果为: [1, 3, 5, 7, 9, 11]
//把每个元素加上2
print(map(partial(op["+"], 2), [0, 2, 4, 6, 8, 10])); //结果为: [2, 4, 6, 8, 10, 12]
来点复杂的:)
提问: 一个二维数组,把其中所有值都求平方
function square(x) {
return x * x;
}
print( map(partial(map, square), [[10, 100], [12, 16], [0, 1]]) );
//结果为: [[100, 10000], [144, 256], [0, 1]]
从上面可以看出,之所以map函数的第一个参数是func而不是数组,是因为可以通过传给partial一个函数来应用map,这种做法把函数从作用于一个值提升到作用于一组值 (表达能力有限,不明白的话请看代码吧-_-)
4. 函数复合(Function Composition)
这个比较简单,回一下之前的适配器函数 negate
function negate(func) {
return function() {
return ! func.apply(null, arguments);
};
}
分析其原理: 调用func,把返回值取反,然后返回。 这种 调用函数A,把结果再经过函数B处理,最后返回B的处理结果 的过程就是函数复合,这种数学概念可以用这个高阶函数表示:
function compose(func1, func2) {
return function() {
return func1(func2.apply(null, arguments));
};
}
var isUndefined = partial(op["==="], undefined);
var isDefined = compose(op["!"], isUndefined);
print(isDefined(Math.PI)); // true
print(isDefined(Math.PIE)); // false
5.
现在,我们可以定义很多新的函数,而完全不用到function关键字,实际工作可能远超出这些简单的例子,而通过稍微的学习,相信FP会让你减少一些加班时间。
推荐几本讲FP和抽象的书
1. Eloquent Javascript (上面例子大多来自该书第六章)
2. The Little Schemer (scheme是FP老祖宗,该书以scheme语言讲的,但就算没学过scheme的人也能看懂)
3. 计算机程序的构造和解释 (满星级推荐)
下一篇将以一个例子介绍函数式编程