【问题标题】:Why is `forever` in Haskell implemented this way?为什么 Haskell 中的“永远”是这样实现的?
【发布时间】:2022-02-04 16:57:51
【问题描述】:

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

forever :: Monad m => m a -> m b
forever ma = ma >> forever ma

但是,在标准库中,函数是 defined 不同:

forever :: Monad m => m a -> m b
forever a = let a' = a *> a' in a'

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

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

【问题讨论】:

  • 我自己没看过,但this article 似乎回答了你的问题。
  • 那篇文章链接到this Q&A,它也讨论了这个问题。

标签: haskell functional-programming monads lazy-evaluation


【解决方案1】:

执行引擎从指向循环的指针开始,并在需要找出接下来要执行的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

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

这次我们不能对前缀进行垃圾回收,因为一个“堆栈帧”向上,我们有一个指向顶级forever的指针,它仍然指向第一个(&gt;&gt;)!哎呀。更高级的定义通过内存循环来解决这个问题。在那里,forever ma 的对象看起来更像这样:

  /----\
 v     |
(*>) --/
 |
 v
ma

现在不需要在执行过程中分配额外的(*&gt;)(也不需要收集垃圾)——即使我们嵌套了它们。执行指针将简单地在此图中四处移动。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-09
    • 2012-10-31
    • 2011-09-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多