泛函编程(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...

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏积累沉淀

linux shell函数定义和调用

说起函数调用,相信大家也不会陌生,然而对于初学Shell的我来说,Shell中函数调用方式却有点让我不太习惯,自己也走了不少的弯路,因为传递参数时出了一个很“自...

2627
来自专栏章鱼的慢慢技术路

《算法图解》第三章笔记与课后练习_递归

1513
来自专栏緣來來來

Python中lambda的学习

这个函数引用时需要传入一个参数,并且有一个返回值。这个参数一般是for x in L之类的传进来,或者直接调用f(3)。

4942
来自专栏星回的实验室

js重修课[四]:函数

函数有两种定义方法:定义表达式如var f = function(){};和声明语句如function f(){}。须知在变量提前这一现象中,声明语句可被提前,...

962
来自专栏黑泽君的专栏

接口中可以包含的组成部分

    1.接口中的抽象方法,修饰符如果自己写必须是:public abstract

6541
来自专栏计算机视觉

Python 函数使用记录,join函数和os.path.join用法

1.join()函数 语法:‘sep’.join(seq) 参数说明: sep:分隔符。可以为空 seq:要连接的元素序列、字符串、元组、字典等 上面的语法即:...

2887
来自专栏和蔼的张星的图像处理专栏

50. 数组剔除元素后的乘积两个遍历

给定一个整数数组A。 定义B[i] = A[0] * ... * A[i-1] * A[i+1] * ... * A[n-1], 计算B的时候请不要使用除法。...

1084
来自专栏西安-晁州

js数组去重

对于如下对象数组 [{id: 0, name: "name1"}, {id: 1, name: "name2"},{id: 1, name: "name2"},...

2410
来自专栏null的专栏

挑战数据结构与算法面试题——统计上排数在下排出现的次数

题目来源“数据结构与算法面试题80道”。在此给出我的解法,如你有更好的解法,欢迎留言。 ? 分析: 本题应该是一个确定的问题,即上排的是个数是题目中给定的...

3206
来自专栏java学习

面试题2(Java 修饰符问题)

编译并运行下面代码,请选择描述正确的项。 public class XiuShiFu{ int i =10; private static float pi=3...

35915

扫码关注云+社区

领取腾讯云代金券