javascript 函数式编程(1)
函数式编程(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. 计算机程序的构造和解释 (满星级推荐)
下一篇将以一个例子介绍函数式编程