延后计算(lazy evaluation)是指将一个表达式的值计算向后拖延直到这个表达式真正被使用的时候。在讨论lazy-evaluation之前,先对泛函编程中比较特别的一个语言属性”计算时机“(strict-ness)做些介绍。strict-ness是指系统对一个表达式计算值的时间点模式:即时计算的(strict),或者延后计算的(non-strict or lazy)。non-strict或者lazy的意思是在使用一个表达式时才对它进行计值。用个简单直观的例子说明吧:

   def lazyFun(x: Int): Int = {
println("inside function")
x + 1
} //> lazyFun: (x: Int)Int
lazyFun(3/0) //> java.lang.ArithmeticException: / by zero

很明显,当我们把 3/0 作为参数传入lazyFun时,系统在进入函数前先计算这个参数的值,计算出现了异常,结果没进入函数执行println就直接退出了。下面我们把lazyFun的参数声明改一下变为:x: => Int:

  def lazyFun(x: => Int): Int = {
println("inside function")
x + 1
} //> lazyFun: (x: => Int)Int
lazyFun(3/0) //> inside function
//| java.lang.ArithmeticException: / by zero
//| at ch5.stream$$anonfun$main$1$$anonfun$1.apply$mcI$sp(ch5.stream.scala:1
//| 0)

在这个例子里我们再次向lazyFun传入了一个Exception。系统这次进入了函数内部,我们看到println("inside function")还是运行了。这表示系统并没有理会传入的参数,直到表达式x + 1使用这个参数x时才计算x的值。我们看到参数x的类型是 => Int, 代表x参数是non-strict的。non-strict参数每次使用时都会重新计算一次。从内部实现机制来解释:这是因为编译器(compiler)遇到non-strict参数时会把一个指针放到调用堆栈里,而不是惯常的把参数的值放入。所以每次使用non-strict参数时都会重新计算一下。我们可以从下面的例子得到证实:

   def pair(x: => Int):(Int, Int) = (x, x)         //> pair: (x: => Int)(Int, Int)
pair( {println("hello..."); 5} ) //> hello...
//| hello...
//| 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例子:

  def if2[A](cond: Boolean, valTrue: => A, valFalse: => A): A = {
if (cond) { println("run valTrue..."); valTrue }
else { println("run valFalse..."); valFalse }
} //> if2: [A](cond: Boolean, valTrue: => A, valFalse: => A)A
if2(true, 1, 0) //> run valTrue...
//| res2: Int = 1
if2(false, 1, 0) //> run valFalse...
//| res3: Int = 0

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)的作用:在真正使时才运算表达式右侧,一旦赋值后不再重新计算。我们试着把上面的例子做些修改:

   def pair(x: => Int):(Int, Int) = {                    //> pair: (x: => Int)(Int, Int)
lazy val y = x //不运算,还没开始使用y
(y,y) //第一个y运算,第二个就使用缓存值了
}

这这个版本里我们使用了一个延缓值(lazy val)y。当调用这个函数时,参数的值运算在第一次使用y时会运算一次,然后存入缓存(cache),之后使用y时就无需重复计算,直接使用缓存值(cached value)。可以看看函数的调用结果:

   pair( { println("hello..."); 5} )               //> hello...
//| res1: (Int, Int) = (5,5)

同样产生了重复值(5,5),但参数值运算只进行了一次,因为只有一行hello...

泛函编程(11)-延后计算-lazy evaluation的更多相关文章

  1. 泛函编程(13)-无穷数据流-Infinite Stream

    上节我们提到Stream和List的主要分别是在于Stream的“延后计算“(lazy evaluation)特性.我们还讨论过在处理大规模排列数据集时,Stream可以一个一个把数据元素搬进内存并且 ...

  2. 泛函编程(12)-数据流-Stream

    在前面的章节中我们介绍了List,也讨论了List的数据结构和操作函数.List这个东西从外表看上去挺美,但在现实中使用起来却可能很不实在.为什么?有两方面:其一,我们可以发现所有List的操作都是在 ...

  3. 泛函编程(19)-泛函库设计-Parallelism In Action

    上节我们讨论了并行运算组件库的基础设计,实现了并行运算最基本的功能:创建新的线程并提交一个任务异步执行.并行运算类型的基本表达形式如下: import java.util.concurrent._ o ...

  4. 泛函编程(38)-泛函Stream IO:IO Process in action

    在前面的几节讨论里我们终于得出了一个概括又通用的IO Process类型Process[F[_],O].这个类型同时可以代表数据源(Source)和数据终端(Sink).在这节讨论里我们将针对Proc ...

  5. 泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink

    上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接 ...

  6. 泛函编程(35)-泛函Stream IO:IO处理过程-IO Process

    IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数 ...

  7. 泛函编程(32)-泛函IO:IO Monad

    由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...

  8. 泛函编程(7)-数据结构-List-折叠算法

    折叠算法是List的典型算法.通过折叠算法可以实现众多函数组合(function composition).所以折叠算法也是泛函编程里的基本组件(function combinator).了解折叠算法 ...

  9. 泛函编程(6)-数据结构-List基础

    List是一种最普通的泛函数据结构,比较直观,有良好的示范基础.List就像一个管子,里面可以装载一长条任何类型的东西.如需要对管子里的东西进行处理,则必须在管子内按直线顺序一个一个的来,这符合泛函编 ...

随机推荐

  1. C#代理那点事儿

    Func代理是啥? Func代理接收0个或多个参数,返回TResult值: 以Func<TSource, TResult>为例:Func带来封装一个方法,该方法接收一个参数,然会一个TRe ...

  2. 【android原生态RPG游戏框架源码】

    转载请注明原创地址:http://www.cnblogs.com/zisou/p/android-RPG.html 这份源码是在今年6月份写的,当时公司有一个技术部们的学习讨论的讲座,然后我自己就写了 ...

  3. [转]ios平台内存常见问题

    本文转自CocoaChina,说的满详细的: 链接地址:http://www.cocoachina.com/bbs/read.php?tid=94017&keyword=%C4%DA%B4%E ...

  4. Android Studio] Gradle项目中添加JNI生成文件(.so文件)

    转:http://blog.csdn.net/qiujuer/article/details/24209457 为了适应潮流使用Android Studio还是有半年多了! 对于从Eclipse迁移项 ...

  5. Activity跳转时生命周期跟踪

    1. 步骤1(打开First Activity):经过onCreate.onStart.onResume后First Activity就展现啦: 2. 步骤2(跳转至Second Activity): ...

  6. UDP网络通信OSC 协议

    使用方法 ofxOscMessage mesg; mesg.setAddress("m"); mesg.addIntArg(); mesg.addIntArg(); mesg.ad ...

  7. WPF RichTextBox设置文本颜色

    //追加 txtResult.Document.Blocks.Add(new Paragraph(new Run("add by run") { Foreground = Brus ...

  8. bash + script

    shell "" 保留$,`,\, 换行含义,‘’保留字面值 $(), ``用于命令替换 算术扩展如 $[1+1] for循环: for Host in host1, host2, ...

  9. WordPress博客搬家注意事项

    博客域名还有一段时间就到期了,准备更换域名和空间,会出现一些问题,我这里在网上收集整理了一下,基本上会遇到两个重要的问题. 首先第一个问题就是数据的搬迁中出现的错误. 我这里用的是phpmyadmin ...

  10. Alsa驱动snd_soc_read的底层实现

    在分析snd_soc_codec_driver的结构体时,发现有些芯片的驱动中定义了字段reg_word_size, reg_cache_size, reg_cache_default,但没有定义re ...