大人者,不失其赤子之心者也。——孟轲

数学家比较幸福的一点是,他们研究的函数是”纯净”的。没有异常,没有意外,同样的入参一定返回同样的结果。如果不是,那就是人出错了。

编程里的函数就没这么纯粹,会遇到无法处理的输入,会在中途拋出异常,在返回值之外会留下别的痕迹。这些不确定性因素,使得人们不能放心大胆地使用函数。因此,需要一个标准,把有这类特点的问题函数识别出来,让”纯函数”发扬光大。

“纯函数”具备以下性质:

  • 确定性 相同的入参,一定返回相同结果,不受内部隐藏状态、隐藏值的影响,也不受任何I/O的影响。多次调用的结果保持一致,说明纯函数具有幂等性。
  • 无副作用 运行函数不能引发可辨别的副作用或输出,如修改可变对象,或写入I/O设备。

展开来讲,纯函数应该避免:

  • 读取函数之外的任何值,如所在类的属性,或全局变量。
  • 修改函数之外的任何对象,如所在类的属性或全局变量。
  • 依赖任何I/O操作,如从本地文件、数据库、API、屏幕等途径读取或写入内容。
  • 修改输入参数。
  • 拋出异常。

回顾之前的定义,对于数学意义上的函数,定义域的元素总能唯一对应值域的元素,加上数学是一种逻辑符号语言,自然没有副作用。那么不难理解,表达这些数学含义的代码函数(scala.math._里定义的函数)也应该是纯函数,如:square, min, max, abs等。或者你自定义的计算,如:

def squareSum(a: Int, b: Int) = a * a + b * b

很多常用类的方法,并不改变方法之外的类属性,也不进行I/O操作,也是纯函数。如String的charAt, isEmpty, length;集合类型的drop(没错,drop也是), filter, map等。

再来看看纯函数的反例。同样是数学上的运算,如果函数拋出异常,就不纯了,因为异常时没有和输入对应的结果。

scala> def divide(x: Int, y: Int): Int =  x / y
divide: (x: Int, y: Int)Int

scala> divide(3, 0)
java.lang.ArithmeticException: / by zero

也不是所有集合类的方法都是纯函数,foreach方法就是专门为副作用准备的。

def foreach[U](f: A => U): Unit

Unit在scala中表示”没有东西”,如果一个函数接受了输入,但不返回任何结果,那么它做的工作就只能是”副作用”了。

常见的非纯函数还有:

  • getDayOfWeek,getHour,getMinute等方法,因为不同时刻调用它们得到的结果不同。
  • scala.util.Random中用于生成随机数的nextInt,因为返回值依赖了输入之外的隐藏状态。
  • I/O操作,如def println(x: Any): Unitdef readLine(): String。所以,当发现签名的入参为空或出参为Unit时,就得擦亮眼睛了。

此外,面向对象语言里常见的setter方法,或改变了所在对象的状态的方法也不是纯函数,如:

object pureFunction {
  var num = 1

  def addNum(delta: Int): Int = {
    num += delta
    num
  }
}

至此,我们已经能辨别纯函数,可纯函数有什么好处呢?