泛函编程(11)-延后计算-lazy evaluation
延后计算(lazy evaluation)是指将一个表达式的值计算向后拖延直到这个表达式真正被使用的时候。在讨论lazy-evaluation之前,先对泛函编程中比较特别的一个语言属性”计算时机“(strict-ness)做些介绍。strict-ness是指系统对一个表达式计算值的时间点模式:即时计算的(strict),或者延后计算的(non-strict or lazy)。non-strict或者lazy的意思是在使用一个表达式时才对它进行计值。用个简单直观的例子说明吧:
1 def lazyFun(x: Int): Int = {
2 println("inside function")
3 x + 1
4 } //> lazyFun: (x: Int)Int
5 lazyFun(3/0) //> java.lang.ArithmeticException: / by zero
很明显,当我们把 3/0 作为参数传入lazyFun时,系统在进入函数前先计算这个参数的值,计算出现了异常,结果没进入函数执行println就直接退出了。下面我们把lazyFun的参数声明改一下变为:x: => Int:
1 def lazyFun(x: => Int): Int = {
2 println("inside function")
3 x + 1
4 } //> lazyFun: (x: => Int)Int
5 lazyFun(3/0) //> inside function
6 //| java.lang.ArithmeticException: / by zero
7 //| at ch5.stream$$anonfun$main$1$$anonfun$1.apply$mcI$sp(ch5.stream.scala:1
8 //| 0)
在这个例子里我们再次向lazyFun传入了一个Exception。系统这次进入了函数内部,我们看到println("inside function")还是运行了。这表示系统并没有理会传入的参数,直到表达式x + 1使用这个参数x时才计算x的值。我们看到参数x的类型是 => Int, 代表x参数是non-strict的。non-strict参数每次使用时都会重新计算一次。从内部实现机制来解释:这是因为编译器(compiler)遇到non-strict参数时会把一个指针放到调用堆栈里,而不是惯常的把参数的值放入。所以每次使用non-strict参数时都会重新计算一下。我们可以从下面的例子得到证实:
1 def pair(x: => Int):(Int, Int) = (x, x) //> pair: (x: => Int)(Int, Int)
2 pair( {println("hello..."); 5} ) //> hello...
3 //| hello...
4 //| res1: (Int, Int) = (5,5)
以上例子里我们向pair函数传入了一段以Int类 5 为结果的代码作为x参数。在返回了结果(5,5)后从两条hello...可以确认传入的参数被计算了两次。
实际上很多语言中的布尔表达式(Boolean Expression)都是non-strict的,包括 &&, || 。 x && y 表达式中如果x值为false的话系统不会去计算y的值,而是直接得出结果false。同样 x || y 中如x值为true时系统不会计算y。试想想如果y需要几千行代码来计算的话能节省多少计算资源。
再看看以下一个if-then-else例子:
1 def if2[A](cond: Boolean, valTrue: => A, valFalse: => A): A = {
2 if (cond) { println("run valTrue..."); valTrue }
3 else { println("run valFalse..."); valFalse }
4 } //> if2: [A](cond: Boolean, valTrue: => A, valFalse: => A)A
5 if2(true, 1, 0) //> run valTrue...
6 //| res2: Int = 1
7 if2(false, 1, 0) //> run valFalse...
8 //| res3: Int = 0
9
if-then-else函数if2的参数中if条件是strict的,而then和else都是non-strict的。
可以看出到底运算valTrue还是valFalse皆依赖条件cond的运算结果。但无论如何系统只会按运算一个。还是那句,如果valTrue和valFalse都是几千行代码的大型复杂计算,那么non-strict特性会节省大量的计算资源,提高系统运行效率。除此之外,non-strict特性是实现无限数据流(Infinite Stream)的基本要求,这部分在下节Stream里会详细介绍。
不过从另一个方面分析:non-strict参数在函数内部有可能多次运算;如果这个函数内部多次使用了这个参数。同样道理,如果这个参数是个大型计算的话,又会产生浪费资源的结果。在Scala语言中lazy声明可以解决non-strict参数多次运算问题。lazy值声明(lazy val)不但能延后赋值表达式的右边运算,还具有缓存(cache)的作用:在真正使时才运算表达式右侧,一旦赋值后不再重新计算。我们试着把上面的例子做些修改:
1 def pair(x: => Int):(Int, Int) = { //> pair: (x: => Int)(Int, Int)
2 lazy val y = x //不运算,还没开始使用y
3 (y,y) //第一个y运算,第二个就使用缓存值了
4 }
这这个版本里我们使用了一个延缓值(lazy val)y。当调用这个函数时,参数的值运算在第一次使用y时会运算一次,然后存入缓存(cache),之后使用y时就无需重复计算,直接使用缓存值(cached value)。可以看看函数的调用结果:
1 pair( { println("hello..."); 5} ) //> hello...
2 //| res1: (Int, Int) = (5,5)
同样产生了重复值(5,5),但参数值运算只进行了一次,因为只有一行hello...