纯函数的概念
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用
比如 slice 和 splice,这两个函数的作用并无二致——但是注意,它们各自的方式却大不同,但不管怎么说作用还是一样的。我们说 slice 符合纯函数的定义是因为对相同的输入它保证能返回相同的输出。而 splice 却会嚼烂调用它的那个数组,然后再吐出来;这就会产生可观察到的副作用,即这个数组永久地改变了
在函数式编程中,我们讨厌这种会改变数据的笨函数。我们追求的是那种可靠的,每次都能返回同样结果的函数,而不是像 splice 这样每次调用后都把数据弄得一团糟的函数,这不是我们想要的。
来看看另一个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13
| var minimum = 21;
var checkAge = function(age) { return age >= minimum; };
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
|
在不纯的版本中,checkAge 的结果将取决于 minimum 这个可变变量的值。换句话说,它取决于系统状态(system state);这一点令人沮丧,因为它引入了外部的环境,从而增加了认知负荷(cognitive load)。
追求纯的理由
可缓存性 (Cacheable)
首先,纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 memoize 技术:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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);
squareNumber(4);
squareNumber(5);
squareNumber(5);
|
值得注意的一点是,可以通过延迟执行的方式把不纯的函数转换为纯函数:
1 2 3
| var pureHttpCall = memoize(function(url, params){ return function() { return $.getJSON(url, params); } });
|
可移植性/自文档化
纯函数是完全自给自足的,它需要的所有东西都能轻易获得。更易于观察和理解,没有偷偷摸摸的小动作。
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
| var signUp = function(attrs) { var user = saveUser(attrs); welcomeUser(user); };
var saveUser = function(attrs) { var user = Db.save(attrs); ... };
var welcomeUser = function(user) { Email(user, ...); ... };
var signUp = function(Db, Email, attrs) { return function() { var user = saveUser(Db, attrs); welcomeUser(Email, user); }; };
var saveUser = function(Db, attrs) { ... };
var welcomeUser = function(Email, user) { ... };
|
这个例子表明,纯函数对于其依赖必须要诚实,这样我们就能知道它的目的。仅从纯函数版本的 signUp 的签名就可以看出,它将要用到 Db
、Email
和 attrs
,这在最小程度上给了我们足够多的信息
可测试性
纯函数让测试更加容易。我们不需要伪造一个“真实的”支付网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。