这里是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实际上可以运行,因为它是不透明的。
我手动尝试永远[单位,双重],永远[单位,字符串]等,这一切都奏效了。这感觉很聪明。
答案 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
电话:
forever(a)
扩展为
{ lazy val t = a flatMap(_ => t) }
扩展为
{ lazy val t = new IO[B] { def run() = { ... t.run } }
由于t
是惰性的,因此只有一次调用2和3中的flatMap
和new 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是如何做正确的。