如何在递归上下文中解释延迟?

时间:2017-04-19 05:47:56

标签: scala functional-programming lazy-evaluation

这里是FPIS

的代码
object test2 {

  //a naive IO monad
  sealed trait IO[A] { self =>
    def run: A
    def map[B](f: A => B): IO[B] = new IO[B] { def run = f(self.run) }
    def flatMap[B](f: A => IO[B]): IO[B] = {
      println("calling IO.flatMap")
      new IO[B] {
        def run = {
            println("calling run from flatMap result")
            f(self.run).run
        }
     }
    }
  }

  object IO {
    def unit[A](a: => A): IO[A] = new IO[A] { def run = a }
    def apply[A](a: => A): IO[A] = unit(a) // syntax for IO { .. }
  }

  //composer in question
  def forever[A,B](a: IO[A]): IO[B] = {
    lazy val t: IO[B] = a flatMap (_ => t)
    t
  }

  def PrintLine(msg: String) = IO { println(msg) }

  def say = forever(PrintLine("Still Going..")).run
}

test2.say 将打印成千上万的" Still Going"在堆栈溢出之前。但我并不确切知道这是怎么发生的。

输出如下:
阶> test2.say
仅调用IO.flatMap //一次 从flatMap结果调用运行结果
还在...... 从flatMap结果调用运行结果
还在继续..

... //重复直到堆栈溢出

当函数 forever 返回时,是否完全计算(缓存)延迟值? 并且,flatMap方法似乎只被调用一次(我添加了print语句),它反对永远的递归定义。为什么呢?

===========
我觉得有趣的另一件事是永远[A,B]中的B型可能是任何东西。 Scala实际上可以运行,因为它是不透明的。

我手动尝试永远[单位,双重],永远[单位,字符串]等,这一切都奏效了。这感觉很聪明。

3 个答案:

答案 0 :(得分:3)

顾名思义,forever方法的作用是使monadic实例a永远运行。更确切地说,它为我们提供了无限的一系列monadic操作。

其值t以递归方式定义为:

t = a flatMap (_ => t)

扩展为

t = a flatMap (_ => a flatMap (_ => t))

扩展为

t = a flatMap (_ => a flatMap (_ => a flatMap (_ => t)))

等等。

Lazy让我们能够定义这样的东西。如果我们删除了惰性部分,我们会得到一个“前向引用”错误(如果递归值包含在某个方法中)或者它只是用默认值初始化而不是递归使用(如果包含在类中,使它成为一个带有幕后getter和setter的类字段。

演示:

val rec: Int = 1 + rec
println(rec) // prints 1, "rec" in the body is initialized to default value 0


def foo() = {
  val rec: Int = 1 + rec // ERROR: forward reference extends over definition of value rec
  println(rec)
}

然而,仅此一点并不是整个堆栈溢出事件发生的原因。有另一个递归部分,这个实际上负责堆栈溢出。它隐藏在这里:

def run = {
  println("calling run from flatMap result")
  f(self.run).run
}

方法run调用自身(请参阅self.run)。当我们这样定义时,我们不会当场评估self.run,因为尚未调用f;我们只是声明一旦调用run()就会调用它。

但是当我们在t中创建值forever时,我们正在创建一个单独使用flat monts的IO monad(它为flatMap提供的函数是“自我评估”)。这将触发run,从而触发f的评估和调用。我们从来没有真正离开flatMap上下文(因此只有一个用于flatMap部分的打印语句),因为一旦我们尝试flatMap,run开始评估函数f,它返回我们调用run的IO,调用run f返回我们调用run的IO,它调用函数f返回我们称之为run的IO ...

答案 1 :(得分:2)

  

我想知道函数永远返回的时候,懒惰的val是否完全计算(缓存)?

  

如果是,那么为什么需要lazy关键字?

在你的情况下没用。它在以下情况下非常有用:

def repeat(n: Int): Seq[Int] {
  lazy val expensive = "some expensive computation"
  Seq.fill(n)(expensive)
  // when n == 0, the 'expensive' computation will be skipped
  // when n > 1, the 'expensive' computation will only be computed once
}
  

我不明白的另一件事是flatMap方法似乎   只调用一次(我添加打印语句)来对抗   永远的递归定义。为什么呢?

在您提供最小,完整且可验证的示例之前无法发表评论,例如@Yuval Itzchakov说

2017年4月19日更新

好吧,我需要纠正自己:-)在你的情况下,由于递归引用回到自身,lazy val是必需的。

为了解释您的观察,让我们尝试展开forever(a).run电话:

  1. forever(a)扩展为

  2. { lazy val t = a flatMap(_ => t) }扩展为

  3. { lazy val t = new IO[B] { def run() = { ... t.run } }

  4. 由于t是惰性的,因此只有一次调用2和3中的flatMapnew IO[B],然后“缓存”以便重复使用。

    在3上调用run()时,你会在t.run上开始递归,从而得到你观察到的结果。

    不完全确定您的要求,但可以实现forever的非堆栈版本:

      def forever[A, B](a: IO[A]): IO[B] = {
        new IO[B] {
          @tailrec
          override def run: B = {
            a.run
            run
          }
        }
      }
    

答案 2 :(得分:2)

  new IO[B] {
    def run = {
        println("calling run from flatMap result")
        f(self.run).run
    }
 }

我现在知道为什么在run方法中出现溢出: def run 中的外部运行调用实际上指向 def run 本身。

调用堆栈如下所示:

f(self.run).run
       |-----|--- println
             |--- f(self.run).run
                         |-----|------println
                               |------f(self.run).run
                                             |------ (repeating)

f(self.run)始终指向相同的评估/缓存延迟val t 对象
因为f:_ =>只需返回 IS 新创建的UNIQUE IO [B]托管我们正在调用的run方法,并将立即再次递归调用。

这就是我们如何在堆栈溢出之前看到print语句。

但是仍然不清楚在这种情况下懒惰的val是如何做正确的。