函数式编程在耳边回响了多年,今天就来详细了解一下它吧。
函数式编程的主要特征是:函数是一等公民。它建议大家写纯函数、没有副作用的函数。
讨论完纯函数的内容,我们会看一下最重要的应用:函数的柯里化。
纯函数的概念
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。其中的副作用指的是:跟函数外部环境发生的交互。包括但不限于:
- 更改文件系统
- 往数据库插入记录
- 发送一个 http 请求
- 可变数据
- 打印/log
- 获取用户输入
- DOM 查询
- 访问系统状态
例如:slice
是纯函数而splice
不是。
纯函数的作用
可缓存性
var memoize = function(f) {
var cache = {};
return function() {
var arg_str = JSON.stringify(arguments);
cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
return cache[arg_str];
};
};
var squareNumber = memoize(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16
值得注意的一点是,可以通过延迟执行的方式把不纯的函数转换为纯函数:
var pureHttpCall = memoize(function(url, params){
return function() { return $.getJSON(url, params); }
});
可移植性/自文档化
纯函数对于依赖很诚实,这样就能知道它的目的。可以说是把所有需要的(可变的)参数都传递进来。
可测试性
简单地给函数一个输入,就能有输出了。
合理性
如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。这样会对理解代码非常重要,因为“可以使用等式推导”(equational reasoning)的技术来分析代码,就是“一对一”替换的情况下手动执行代码。
并行代码
最后一点,也是决定性的一点:我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。
并行代码在服务端 js 环境以及使用了 web worker 的浏览器那里是非常容易实现的,因为它们使用了线程(thread)。不过出于对非纯函数复杂度的考虑,当前主流观点还是避免使用这种并行。
合并
合并就是一个值经过多个函数变化最后输出结果。很明显是一个 reduce 的概念。
比如 a(b(c(d)))
看起来很不舒服,很像 JS 里的回调地域有特别多的大括号。所以我们要是有一个 compose
函数,可以将表达式写成 [c, b, a].compose(d)
该多好,而且如果数组里的函数都是纯函数,甚至还能满足结合律。
function compose(array) {
return function(args) {
return array.reduce(function (total, f) {
return f(total);
}, args);
}
}
或者
function compose(array) {
return function(args) {
let result = args;
array.forEach(function(f) {
result = f(result);
});
return result;
}
}
柯里化
curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
function curry(fn) {
return function finalCurry(...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return function (...args2) {
return finalCurry(...args, ...args2);
}
}
}
}