用Scala实现延迟计算

背景

上次重构用到了Stream,他的延迟计算能力很酷! 于是乎查阅了一下延迟计算的原理——没想到这看似魔法一般的延迟计算,其实现原理却是是这么的简单!
本文依照其原理用Scala语言实现了一下。本文只是对延迟计算原理的探讨,在Scala里它直接在语言级提供了lazy关键字,可以用来定义延迟计算,所以实际应用时用lazy就好了。

知识点

1. 延迟计算
2. 叫名参数(Call By Name)
3. 匿名函数

实现原理

延迟计算看似魔法般的存在,实际上在函数式编程里,其实现是如此的简单直白:

① 延迟不过是一个无参匿名函数的语法糖:

def delay[A](v: => A) = {() => v}

② 而求值的过程,不过是对这个匿名函数的调用:
def compute[A](dv: () => A) = dv()

以上两个简单的函数即完成了延迟计算的全部!
用以下代码测试一下,可以看到我们的x和y是在调用compute时才真正开始计算的,达到了延迟计算的效果:

例1:

scala> import java.util.Calendar
import java.util.Calendar

scala> val x = delay(Calendar.getInstance.getTime)
x: () => java.util.Date = <function0>

scala> Calendar.getInstance.getTime
res0: java.util.Date = Wed Jul 30 14:14:43 CST 2014

scala> compute(x)
res1: java.util.Date = Wed Jul 30 14:14:55 CST 2014


例2:

scala> val y = delay((1 to 1000000000).toList.filter(_ % 3 == 0).take(4))
y: () => List[Int] = <function0>

scala> compute(y)
java.lang.OutOfMemoryError: Java heap space


delay实现详解

def delay[A](v: => A) = {() => v}

  1.  参数的类型要为 "=> A",这在scala里表示参数为Call By Name。与之对应的,参数类型写为 A 的时候,表示Call By Value。这两者的区别,简单来说就是:
         ① Call By Name: 参数在函数中用到的时候才求值
         ② Call By Value: 参数先求值,然后才把值传到函数中去
         注:这两种方式实际上涉及到函数式编程里一个基本的概念:代换模型。在SICP这本书中有详细的介绍。Call By Name对应于SICP中的正则序求值,即先展开再规约;Call By Value对应于SICP中的应用序求值。Scala和Scheme、Lisp等一样,默认采用按应用序求值,即Call By Value的方式。在Scala里如果想使用Call By Name的方式,只需要简单的在参数类型前面加个"=>"

  2. 函数体为 {() => v}: 这里实际上是定义了一个无参匿名函数,这个匿名函数的返回值为v,并把这个匿名函数作为delay函数的返回值。

         ① 什么是匿名函数? 比如最常用的List(1,2,3).map(x => x * 2)中,x => x * 2就是一个匿名函数。这个匿名函数有一个入参x,函数体为 x * 2。在函数式编程的世界里,这个匿名函数更专业的叫法为lambda表达式,lambda表达式的返回值是返回一个新的函数。lambda演算是函数式语言的数学基础。最早的函数式语言Lisp就是John McCarthy在做lambda演算论文的时候不小心创造出来的,所以说函数式语言从一开始就更接近数学,更接近高度的抽象;而不是像C、C++这些命令式语言一开始就面向机器硬件。

         ② delay函数的返回值是另外一个函数!这就是实现延迟计算的奥秘!——你在定义一个延迟计算变量delay(x)的时候,其实你什么都没干,只是把这个x包装到了一个函数中而已;直到你需要用它的时候,才在它上面应用compute方法,开始真正的计算。

对象方式的实现

在Scala的世界里,函数就是对象,对象某种程度上也可以看成函数,这也是Scala的完美融合两者的强大之处吧。下面是“面向对象”的一种实现方式:

class Delay[A](v: => A) {
 def compute() = v
}


应用示例:

scala> val x = new Delay(Calendar.getInstance.getTime)
x: Delay[java.util.Date] = Delay@a80d36

scala> Calendar.getInstance.getTime
res32: java.util.Date = Wed Jul 30 16:20:01 CST 2014

scala> x.compute
res33: java.util.Date = Wed Jul 30 16:20:12 CST 2014


优化:带记忆的版本

目前的实现在每次调用compute的时候,都要重新计算。对于计算量很大的运算,会损失性能;同时,如上面的例子,每次调用compute的返回值是不一样的,这也不符合我们对一个val直觉的认识。因此,大部分语言延迟计算的实现都有一个重要的优化:只做一次运算。Scala里的lazy也是这样的,只会做一次计算。

要实现只做一次运算,要求我们的Delay要有记忆:当第一次执行的时候做运算,执行后记住运算结果,下次再执行compute时直接返回上次的运行结果。实现如下:

class Delay[A](v: => A) {
  private var isFirstRun = true
  private var value: A = _
  def compute() = {
    if (isFirstRun) {
      value = v
      isFirstRun = false
    }
    value
  }
}


应用示例:

val x = new Delay(Calendar.getInstance.getTime)
x: Delay[java.util.Date] = Delay@13455e7

scala> Calendar.getInstance.getTime
res36: java.util.Date = Wed Jul 30 16:35:46 CST 2014

scala> x.compute
res37: java.util.Date = Wed Jul 30 16:35:52 CST 2014

scala> x.compute
res38: java.util.Date = Wed Jul 30 16:35:52 CST 2014


 

 

 

 

 

 

 

转载于:https://my.oschina.net/guanxun/blog/305528

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值