函数式编程(一):纯函数
其他:
函数式编程(二):curry
前言:这个系列是在读《JS函数式吧编程指南》时,做的笔记。
函数式编程与面向对象编程
首先这两种思想不一定就是互斥的,就像 JavaScript,Ruby,Python 也是同时柔和了两种思想的好处。的确,两样东西看起来像是不同层面的东西,函数式编程对应的应该是指令式编程,而面向对象编程更多是一种抽象现实模型的一种设计思路。
面向对象强调数据与行为的绑定,行为(方法)围绕着数据来展开。而函数式编程则倾向于函数与数据的等价,所谓的函数“一等公民”,也就是函数也被看作一种数据,可以作为参数,可以作为返回值。
函数式编程在理论上基于 Lambda 演算,而指令式编程基于图灵机。现在的机器的指令集都是基于图灵机的,也就是硬件实现上都是基于图灵机,函数式编程最后还得编译为图灵机的二进制。函数式的执行效率不高。
函数式编程的特点:表达式化,高阶逻辑(告诉计算机做什么,而不是怎么做)组合子逻辑(自底向上设计)
什么是纯函数?
纯函数是这样一种函数,即相同的输出,永远会得到相同的输出,而且没有任何可观察的副作用。相同的输入,永远会得到相同的输出,意味着对外部状态的解耦。
‘副作用’是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行可观察的交互。概括来讲,只要跟函数外部环境发生的交互就都是副作用。函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。并不是说,要禁止使用一切副作用,而是,要让他们在可控的范围内发生。从定义上来说,纯函数必须能根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,那么就无法保证这一点了。意味着函数要与外部系统状态打交道,提倡函数式编程的人认为,这种共享状态导致的混乱是绝大多数bug的万恶之源
var xs = [1,2,3,4,5]; // 纯函数 xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3] // 非纯函数 xs.splice(0,3); //=> [1,2,3] xs.splice(0,3); //=> [4,5] xs.splice(0,3); //=> []
// 不纯的 var minimum = 21; var checkAge = function(age) { return age >= minimum; }; // 纯函数 var checkAge = function(age) { var minimum = 21; return age >= minimum; };
在不纯的版本中,函数的结果取决于 minimum 这个可变变量的值。换句话说,它取决于系统状态。输入值(参数)之外的因素能够左右函数的返回值,会使函数变得不纯,导致我们思考整个软件是总是要面对函数外部复杂多变的系统状态。
如果一个函数是纯函数,它的输入就直接表明了输出,那么就没有必要实现具体的细节了,因为函数仅仅只是输入到输出的映射。基于这个概念,就可以逐步构建出函数式编程的蓝图。
传统的命令式编程方法和过程都深深地植根于它们所在的环境中,通过状态、依赖和有效作用达成。而纯函数则与环境无关,可以在任何地方运行它,并保证相同的输入总会有相同的结果。
#纯函数的好处
1.引用透明性
所谓的引用透明性就是一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下,替换的。而纯函数就是引用透明的。引用透明意味着可以进行等式推导(就像高中数学的推导),等式推导可以为重构和理解代码带来相当大的分析能力。
//太傻 var getServerStuff = function(callback) { return ajaxCall(function(json) { return callback(json); }); }; //这才像样 var getServerStuff = ajaxCall; //因为 return ajaxCall(function(json) { return callback(json); }); //等价于 return ajaxCall(callback(json)); //而 var getServerStuff = function(callback) { return ajaxCall(callback(json)); }; //就等于... var getServerStuff = ajaxCall;
2.并行代码
纯函数不依赖于系统状态的特点,我们就可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态。