泛函编程(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 条评论
登录 后参与评论

相关文章

来自专栏前端架构

JS中的bind的用法与注意事项

    this.dom = document.getElementById("scroll");

835
来自专栏我是业余自学C/C++的

redis_3.0.7_sds.c_sdsIncrLen()

752
来自专栏hbbliyong

C 语言 static、extern与指针函数介绍

1.exit(0)正常退出程序    exit(1)程序异常时退出程序 2.static(静态变量)修饰局部变量   在局部变量使用static修饰,会延长局部...

3248
来自专栏JetpropelledSnake

Python入门之python可变对象与不可变对象

本文分为如下几个部分 概念 地址问题 作为函数参数 可变参数在类中使用 函数默认参数 类的实现上的差异 概念 可变对象与不可变对象的区别在于对象本身是否可变。 ...

2806
来自专栏Java架构师学习

为Java程序员金三银四精心挑选的五十道面试题与答案

1、面向对象的特征有哪些方面? 【基础】 答:面向对象的特征主要有以下几个方面: 1)抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地...

3396
来自专栏JetpropelledSnake

Python入门之面向对象编程(三)Python类的魔术方法

python类中有一些方法前后都有两个下划线,这类函数统称为魔术方法。这些方法有特殊的用途,有的不需要我们自己定义,有的则通过一些简单的定义可以实现比较神奇的功...

2724
来自专栏个人分享

Scala语法笔记

JAVA中,举例你S是一个字符串,那么s(i)就是该字符串的第i个字符(s.charAt(i)).Scala中使用apply的方法

782
来自专栏Golang语言社区

厚土Go学习笔记 | 26. 函数闭包

如果非必要,尽量不要在程序中使用闭包。 go函数可以是一个闭包。闭包是一个函数值,它引用了函数体之外的变量。这个函数可以对这个变量进行访问和赋值。 展示一个例子...

34713
来自专栏java一日一条

Java Lambda 表达式学习笔记

Java Lambda 表达式是 Java 8 引入的一个新的功能,可以说是模拟函数式编程的一个语法糖,类似于 Javascript 中的闭包,但又有些不同,主...

361
来自专栏小樱的经验随笔

【Python学习笔记之三】lambda表达式用法小结

除了def语句之外,Python还提供了一种生成函数对象的表达式形式。由于它与LISP语言中的一个工具很相似,所以称为lambda。就像def一样,这个表达式创...

2615

扫码关注云+社区