为什么 Haskell 中的“永远”是这样实现的?

Nik*_*nov 10 monads haskell functional-programming lazy-evaluation

Haskell 提供了一个方便的函数forever,可以无限地重复单子效果。它可以定义如下:

forever :: Monad m => m a -> m b
forever ma = ma >> forever ma
Run Code Online (Sandbox Code Playgroud)

然而,在标准库中该函数的定义有所不同:

forever :: Monad m => m a -> m b
forever a = let a' = a *> a' in a'
Run Code Online (Sandbox Code Playgroud)

let 绑定用于强制“此处显式共享,因为无论优化如何,它都可以防止空间泄漏”(来自实现的评论)。

您能解释一下为什么第一个定义可能存在空间泄漏吗?

Dan*_*ner 10

执行引擎从指向循环的指针开始,并在需要找出IO下一步要执行的操作时延迟扩展它。根据您的定义forever,以下是循环的几次迭代,就像“存储在内存中的对象”一样:

1.
  PC
   |
   v
forever
   |
   v
  ma

2. 
PC
 |
 v
(>>) --> forever
 |         /
 v L------/
ma

3.
           PC
            |
            v
(>>) --> forever
 |         /
 v  L-----/
ma

4.
         PC
          |
          v
(>>) --> (>>) --> forever
 |        /          /
 v L-----/----------/
ma

5 and 6.
                  PC
                   |
                   v
(>>) --> (>>) --> (>>) --> forever
 |        /        /          /
 v L-----/--------/----------/
ma
Run Code Online (Sandbox Code Playgroud)

结果是,随着执行的继续,您将获得越来越多的单元格副本(>>)。正常情况下,这没什么大不了的;没有对第一个单元格的引用,因此当发生垃圾收集时,已执行的前缀将被丢弃。但是如果我们不小心传递了一个无限循环怎么办ma

1.
  PC
   |
   v
forever
   |
   v
forever
   |
   v
  ma

2.
  PC
   |
   v
  (>>) -> forever
   |         /
   v L------/
forever
   |
   v
  ma

3.
                 return here
                 when done
                     |
                     v
         (>>) --> forever
          |          /
          v L-------/
PC --> forever
          |
          v
         ma

4.
               return here
                   |
                   v
       (>>) --> forever
        |          /
        v L-------/
PC --> (>>) --> forever
        |          /
        v L-------/
       ma

like, 12ish.
       return here
            |
            v
(>>) --> forever
 |          /
 v L-------/
(>>) --> (>>) --> (>>) --> (>>) --> (>>) --> forever <-- PC
 |        /        /        /        /          /
 v L-----/--------/--------/--------/----------/
ma
Run Code Online (Sandbox Code Playgroud)

这次我们无法对前缀进行垃圾收集,因为一个“堆栈帧”向上,我们有一个指向顶层的指针forever,它仍然引用第一个(>>)!哎呀。更奇特的定义通过创建内存循环来解决这个问题。在那里,forever ma的对象看起来更像是这样的:

  /----\
 v     |
(*>) --/
 |
 v
ma
Run Code Online (Sandbox Code Playgroud)

现在,(*>)随着执行的进行,不需要额外分配(也不需要垃圾收集)——即使我们嵌套它们。执行指针将简单地在该图中来回移动。