谈谈惰性求值

简述

一些不了解函数式编程的人, 可能以为一个函数如果有很多层 map map filter ... 等高阶函数处理,会不断创建很多个新的 List 去处理, 对函数的执行效率有影响。 可是结果真的是这样的吗?

假设我们有一个 List,要求 + 40 并去掉奇数 然后 * 3, scala 函数:

scala> List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3)
res0: List[Int] = List(36, 42)

上面的表达式中 map(_ + 10)会产生一个中间列表, 然后对中间列表进行 filter(_ % 2 == 0)也会产生一个列表, 接下来再进行map(_ * 3)操作产生最终的列表。 每一次的转换都产生了一个临时列表, 作为下次的输入。 我们来追踪下函数的求值过程:

List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3)
List(11,12,13,14).filter(_ % 2 == 0).map(_ * 3)
List(12,14).map(_ * 3)
List(36,42)

这里展开了对表达式每次执行求值结果的替代, 每次遍历自身, 对每个元素使用输入的函数就行求值, 并输出一个重新分配的列表。 如果我们对一系列的转换融合成单个函数,而避免产生临时List数据结构岂不美哉?可以用个 for 循环手动重写,但是理想中的情况是能够自动实现, 并且保留高阶组合风格。 惰性求值来了!

严格和非严格

在 scala 中是这么定义严格求值和非严格求值的

非严格求值是函数的一种属性, 称一个函数式非严格求值的意思是这个函数可以选择不对它的一个和多个参数求值。 而严格函数总是对它的参数求值。 大多数编程语言支支持严格求值, 在 scala 中需要明确声明。

你可能会对非严格求值的概念比较熟悉, 我们来看个 js 的例子:

false && console.log("It's false!")

上面并不会打印任何内容, console.log 函数没有执行, 已经有些非严格求值的概念了。 来看一个非严格求值的 if 函数, 可以说很熟悉了:

def if1[A](cond: Boolean, onTrue: => A, onFalse: => A): A =
  if (cond) onTrue else onFalse

if1(false,
  sys.error("err!"),
  3)

给函数传递一个未求值的参数,在函数体中引用的地方会被求值一次, 则不会缓存一个函数的求值结果:


scala> val callx = { println("x"); 15 }
x
callx: Int = 15

scala callx
res0: Int = 15

scala 中一共了 lazy 关键字, 会时 scala 对这个变量延迟求值, 知道它第一次被引用的时候, 它也会缓存结果, 在后续引用的地方不用触发重复求值

scala> lazy val cally = { println("y"); 16 }
cally: Int = /<lazy>
scala cally
y
res1: Int = 16

scala cally
res1: Int = 16