【问题标题】:Funky haskell lazy list implicit recursion时髦的 haskell 惰性列表隐式递归
【发布时间】:2011-04-22 16:18:53
【问题描述】:

在 Haskell 中,由于懒惰,您可以构建无限列表:

Prelude> let g = 4 : g
Prelude> g !! 0
4
Prelude> take 10 g
[4,4,4,4,4,4,4,4,4,4]

现在,当我尝试构建这样的列表时究竟发生了什么?

Prelude> let f = f !! 10 : f
Prelude> f !! 0
Interrupted.
Prelude> take 10 f
[Interrupted.
Prelude>

Interrupted.s 是我在等待几秒钟后按下 CTRL+C。它似乎进入了一个无限循环,但为什么会这样呢?


非Haskellers的解释:

: 运算符是prepend

Prelude> 4 : [1, 2, 3]
[4,1,2,3]

这一行:

Prelude> let g = 4 : g

说“让g 成为通过将4 前置到列表g 中构造的列表”。当您请求第一个元素时,将返回 4,因为它已经存在。当您请求第二个元素时,它会查找 4 之后的元素。该元素将是列表 g 的第一个元素,我们刚刚计算了 (4),因此返回了 4。下一个元素是g 的第二个元素,我们再次计算,等等...

!! 只是索引到一个列表中,所以这意味着从g 获取索引0 处的元素:

Prelude> g !! 0
4

但是当我这样做时:

Prelude> let f = f !! 10 : f

有些东西坏了,因为要计算f 的第一个元素,您需要第 11 个元素,它还不存在?不过,我希望出现异常,而不是无限循环...

【问题讨论】:

  • 对于踢球,请尝试以下方法:length $ take 10 f。一个字:Thunks。也可以试试:length [error "raises an error"].
  • 我建议您删除非Haskellers部分的说明。
  • @Wei Hu:如果有很多人点赞你的评论,我会这样做

标签: haskell lazy-evaluation


【解决方案1】:

在这种情况下,一张图片可能会说出一千个单词。

首先,记住 cons((:) 列表构造函数)是如何工作的。这是一对两件事:一个元素,以及对列表尾部的引用(这是另一个缺点,或 [])。

您应该知道,当您说[1, 2, 3] 时,它只是(1:(2:(3:[])))1:2:3:[] 的快捷方式。如果您将每个 cons 对可视化为一个带有两个插槽的盒子,则此表达式如下所示:

┌───┬──┐  ┌───┬──┐  ┌───┬──┐  ┌────┐
│ 1 │ ─┼─>│ 2 │ ─┼─>│ 3 │ ─┼─>│ [] │
└───┴──┘  └───┴──┘  └───┴──┘  └────┘

一个循环

当您说g = 4 : g 时,您实际上并不是在构建一个“无限”列表,而是在构建一个循环 列表:g 被定义为一个 cons,其尾部引用仅指向回到g本身

┌──────────┐
│ ┌───┬──┐ │
└>│ 4 │ ─┼─┘
  └───┴──┘

这实际上与懒惰无关,而一切都与自我引用有关:例如,您可以在(渴望)Common Lisp 中使用'#1=(4 . #1#) 之类的语法(其中#1 就像@987654333 @)。

无论你说g !! 0,还是g !! 1000000000000g 永远不会增长:(!!) 只是在循环中原地运行,按照你告诉它的次数运行,直到它耗尽自己并返回元素,4

两个循环

当你说f = (f !! 10) : f 时,同样的事情会发生——除了现在,元素槽包含与4 不同的表达式:

┌──────────┐
│ ┌───┬──┐ │
└>│ ╷ │ ─┼─┘
  └─┼─┴──┘
    │
    │ ┌───────────┐
    └>│ (f !! 10) │
      └───────────┘

至关重要的是,这个子表达式指向f,就像尾巴一样:

 ┌──────────┐
 │ ┌───┬──┐ │
┌┴>│ ╷ │ ─┼─┘
│  └─┼─┴──┘
│    │
│    │ ┌───────────┐
│    └>│ (f !! 10) │
│      └──┼────────┘
└─────────┘

因此,当您请求 f !! n 时,(!!) 将首先在顶部循环 n 周围运行,然后返回元素,就像 g 所做的那样。然而,(f !! 10) 并没有退出循环,而是重新进入它,并且这个过程会不断重复:在顶部循环 10 次,然后在底部循环一次,然后返回。

【讨论】:

  • 我为你的 ASCII 技术喝彩
  • 哦,呵呵,我没有意识到如果我从指针的角度考虑它会那么容易。而且我没有意识到这只是循环......所以我想懒惰是因为没有试图立即评估f !! 10(例如,只有当我做f !! 0时)。
  • Claudiu:对,这就是懒惰的来源。从这个意义上说,说f = (f !! 10) : f 的所有意图和目的都与说f = undefined : f 相同。
【解决方案2】:

“尚不存在”并不完全正确。我们尽量不去想什么时候值存在——指称编程是关于永恒不变的值和方程。

更具体地说,这段代码很好:

Prelude> let x = [(x !! 1) + 1, 3] in x
[4,3]

您可能认为x !! 1 还不存在。但下面是 GHC 等实现的工作原理。

在构建f 列表时,它会构造一个内存对象(“t​​hunk”)来表示表达式(x !! 1) + 1。尚未对 x 进行评估。它将指向此 thunk 的指针和指向 3 的指针包装到一个链表中,并将其交给 GHCi 的隐式 show

现在,列表的show 实例必须一一显示元素。 show 将要求(“强制”)评估 (x !! 1) + 1 的 thunk,这会导致该代码被“输入”。根据(+) 的定义,强制(x !! 1) + 1 强制x !! 1,这反过来又强制 件事:

  • 列表的 spine(其 cons 单元格)通过第二个单元格,并且
  • 第二个元素的(在 Lisp 术语中,第二个单元格的“汽车”),因为(+) 想要对该值进行操作。

现在第二个值出现了——它是3。如果它是另一个重击,我们会强迫它,依此类推。请参阅this blog post,了解有关自引用容器的有趣观点。

现在,编译的 GHC 代码如何在您的另一个示例中检测到无限循环?当我们输入一个 thunk 时,我们需要记住稍后再回来并用最终值覆盖它。这就是“惰性求值”相对于“非严格语义”的具体含义,它可以防止我们重复工作。

无论如何,作为输入 thunk 时的优化,GHC 的运行时将首先用另一个称为“黑洞”的对象覆盖它。我们稍后会回来并用最终值覆盖黑洞。但是如果我们在进入黑洞之前会发生什么呢?这意味着评估x 需要首先评估x,这是一个无法解决的循环。所以进入黑洞会抛出异常。

【讨论】:

  • 是的,我希望您给出的示例能够工作,甚至在阅读说明之前。不过,一步一步很好!现在没有黑洞的东西,让我试着看看它为什么会陷入无限循环。所以f = f !! 10 : f 展开时的结果将是一个列表,其中每个元素都等于第 11 个元素的值。在某些时候,我们必须计算第 11 个元素的值。这需要计算第 11 个元素,这需要计算第 11 个元素,等等等等。是循环卡住的地方吗?
  • 次要的 nitpick:在 Lisp 术语中,cons-list 中第二个元素的值将是 cadr 不是吗?我认为这辆车就是尾巴。
  • @mokus 我的意思是那个特别的缺点牢房的车。为清晰起见进行了编辑。
【解决方案3】:

你对它为什么挂起是正确的——你创建了一个它无法解决的循环依赖。计算当前元素需要后面的元素,直到计算当前元素才能计算出来,等等等等,我们绕着圈子走。

至于为什么不产生异常,尝试编译而不是在GHCi中运行:

$ ghc --make Loop.hs
$ ./Loop.exe
Loop.exe: <<loop>>

我假设这是NonTermination exception。噗,停机问题?哈。

在 GHCi 中完成时,并非一切都按照您希望或期望的方式运行。如果有些事情看起来很奇怪,请尝试编译一个小示例,看看这样是否更有意义。例如,使用不同的类型默认规则的 GHCi 有时会让我不快。

【讨论】:

    猜你喜欢
    • 2017-02-18
    • 2019-07-21
    • 2015-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-16
    • 1970-01-01
    相关资源
    最近更新 更多