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. 计算机程序的构造和解释 (满星级推荐)

 

下一篇将以一个例子介绍函数式编程

 

 

posted @ 2013-06-23 01:02  aj3423  阅读(366)  评论(0编辑  收藏  举报