关于函数式编程的一点儿思考 - pod4g/tool GitHub Wiki

函数式编程一个重要的概念是纯函数(pure function),即对应数学中的函数的概念y = f(x),对于唯一的输入x,一定输出唯一的y,不管是调用f(x)多少次,只要是x相等,那么y一定是相等的。 纯函数与外界交换数据只有一个唯一渠道——参数和返回值 纯函数最重要的特点是无副作用(side effect),即不会改变全局。

来自维基百科的定义:

在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:

  • 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
  • 纯函数的输出可以不用和所有的输入值有关,甚至可以和所有的输入值都无关。但纯函数的输出不能和输入值以外的任何资讯有关。纯函数可以传 回多个输出值,但上述的原则需针对所有输出值都要成立。若引数是传引用调用,若有对参数物件的更改,就会影响函数以外物件的内容,因此就不是纯函数。

下面用代码来表示:

// 纯函数
function f(x){
  return x + 1
}

var y  = f(1) // 2
var y2 = f(1) // 2

// 不纯函数
var x = 1
function f(){
  return ++x
}
var  y = f() // 2
var y2 = f() // 3
...

这是最简单的形式,即x为基本类型。但是在编程的世界远不止这么简单,还有一种叫做引用类型的值。

比如:

var arr = [1, 2, 3]
// 其实由于arr是引用值,传不传参数都一样
function changeArr(/* arr, */ value){
  arr.push(value)
  return arr
}
var ret = changeArr(/* arr, */ 4)
console.log(arr) // [1, 2, 3, 4]
console.log(ret) // [1, 2, 3, 4]
console.log(arr === ret) // true
var ret2 = changeArr(/* arr, */ 5)
console.log(arr) // [1, 2, 3, 4, 5]
console.log(ret) // [1, 2, 3, 4, 5]
console.log(arr === ret2) // true
// arr对象的位置(即引用地址)没变,但是内部含有的数据变了,那么changeArr算是纯函数么?

按照纯函数的定义来说,这就不是纯函数,因为改变了arr全局变量,虽然引用地址没变,但是这个对象本身的数据发生了变化。

那么如果涉及到引用类型的,怎么写才算函数是的呢?


var arr = [1, 2, 3]

function deepClone(obj){
  return JSON.parse(JSON.stringify(obj))
}

function changeArr(arr, value){
  arr = deepClone(arr)
  arr.push(value)
  return arr
}

var ret = changeArr(arr,4)
console.log(arr) // [1, 2, 3]
console.log(ret) // [1, 2, 3, 4]
console.log(arr === ret) // false
var ret2 = changeArr(arr,5)
console.log(arr) // [1, 2, 3]
console.log(ret) // [1, 2, 3, 4]
console.log(arr === ret2) // false
// 虽然每次都重新拷贝了一个arr导致ret与arr不再相等,但是每次返回过来的数据都是一致的。这就是纯函数

纯函数遇到引用类型最容易出现的误解就是认为每次返回的数据只要用===判断相等他就是的, 其实,这里的相同不是 === 的比较,而是数据结构的相同(或比较),可以认为是每次都深拷贝了一次。

所以我们可以用深拷贝的技术实现纯函数。

但是想实现纯函数必须要有deepEqual的判断,这是基于数据结构的比较而不是引用值的比较(用===即是引用值的比较)。

比如在immutable.js中就是使用obj.equal(obj2)来进行deepEqual判断的。 当然使用deepCopy你也能实现immutable.js,但是出于性能上的考虑,immutable.js并没有这么做,这就是后话了。


2016-12-27更新

在软件技术中,还有一个词叫做幂等性,它跟纯函数的概念很像

幂等性的定义:

一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等操作对于代理和缓存来说具有“友好性”,因为幂等操作的额外执行不会对二者产生危害性后果(除了带宽浪费)。

换句话说,某个操作具有幂等性,就是这个操作执行一次和执行多次对系统内部的状态影响是一样的。不过我们初始化了Person对象‘老王',然后执行老王.setAge(56)设置老王的年龄,无论老王.setAge(56)执行多次,对老王这个对象的内部状态的影响是一样的,因此说Person的setAge()方法是幂等的。

两者的区别:

  1. 纯函数的概念主要存在于函数式编程这种编程范式中,而幂等性则比较普适,在面向对象范式中,如果一个方法调用多次对内部的状态影响是一样的,则这么方法就具有幂等性,在函数式编程中,纯函数也具有幂等性,但具有幂等性的函数却不一定是纯函数,还有我们常说GET/PUT/DELETE是幂等的

  2. 纯函数主要强调相同的输入,多次调用,输出也相同且无副作用,而幂等主要强调多次调用,对内部的状态的影响是一样的,也就是说,对于幂等,多次调用返回值可能不同,但由于对内部的状态影响是一样的,故其幂等