Loops “好奇如何”;loop=loop";在Haskell中进行评估

Loops “好奇如何”;loop=loop";在Haskell中进行评估,loops,haskell,recursion,evaluation,infinite-loop,Loops,Haskell,Recursion,Evaluation,Infinite Loop,我以为这样的表达式会让Haskell永远计算。但GHCi和编译程序中的行为令我惊讶 例如,在GHCi中,这些表达式一直阻塞到IControl+C,但不消耗CPU。看起来它在睡觉 let loop = loop let loop = 1 + loop 我尝试用GHC编译这些程序: main = print loop where loop = 1 + loop main = print loop where loop = if True then loop else 1 印刷的是: M

我以为这样的表达式会让Haskell永远计算。但GHCi和编译程序中的行为令我惊讶

例如,在GHCi中,这些表达式一直阻塞到I
Control+C
,但不消耗CPU。看起来它在睡觉

let loop = loop
let loop = 1 + loop
我尝试用GHC编译这些程序:

main = print loop
  where loop = 1 + loop

main = print loop
  where loop = if True then loop else 1
印刷的是:

Main: <<loop>>
Main:
所以我的问题是:显然,这些表达式被编译成不同于命令式语言中的循环或递归调用。它们被编译成什么?这是一个特殊的规则来处理0-arg函数,它们自己在右边,还是一个更一般的特殊情况,我不知道

[编辑]:

还有一个问题:如果这恰好是编译器的特殊处理,那么在无法检查所有无限循环的情况下执行此操作的原因是什么熟悉的“语言”不关心像
while(true)这样的情况
intf(){return f();}
,对吗


非常感谢。

在某些有限的情况下,编译器可以在其其他控制流分析中确定是否存在这样的循环,并在此时使用引发适当异常的代码替换循环项。当然,这不是在所有情况下都能做到的,但只有在一些更明显的情况下才能做到,在这些情况下,编译器正在做的其他工作自然会导致这种情况

至于为什么Haskell比其他语言更经常发现这一点:

  • 这些情况不会发生在像C这样严格的语言中。当惰性变量的计算依赖于它自己的值时,这些循环就会发生
  • 像C这样的语言在循环中有非常特殊的语义;也就是说,按什么顺序做什么。因此,他们被迫实际执行循环。但是,Haskell定义了一个特殊的值
    124;
    (“底部”),用于表示错误的值。严格要求自身的值(即,它们依赖于自己的值进行计算)是
    124;
    上的模式匹配结果可以是无限循环,也可以是异常;您的编译器在这里选择后者
  • Haskell编译器对执行严格性分析非常感兴趣,即证明某个表达式依赖于某些其他表达式,以便执行某些优化。这种循环分析作为严格度分析器中的一种边缘情况自然出现,必须以某种方式进行处理

GHC将Haskell实现为一个图形归约机。将您的程序想象为一个图形,每个值都是一个节点,从它到该值所依赖的每个值都有一行。除此之外,我们是懒惰的,所以您实际上只从一个节点开始——要计算该节点,GHC必须“输入”它并将其打开给一个带参数的函数。然后,它用函数体替换函数调用,并尝试将其减少到足够的程度,使其成为head标准形式,等等

以上内容非常简单,为了简洁起见,我肯定会省略一些必要的细节

在任何情况下,当GHC输入一个值时,它通常会在评估节点时(或者,根据您的术语,在减少闭包时)将其替换为一个黑洞。这有许多目的。首先,它堵塞了潜在的空间泄漏。如果节点引用了一个在其他地方没有使用过的值,那么即使在对节点求值时,黑洞也允许对该值进行垃圾收集。其次,这可以防止某些类型的重复工作,因为在多线程环境中,两个线程可能会尝试输入相同的值。黑洞将导致第二个线程阻塞,而不是计算已被计算的值。最后,这恰好允许有限形式的循环检测,因为如果线程试图重新进入自己的黑洞,我们可以抛出异常

这里有一个更具隐喻性的解释。如果我有一系列的指令在屏幕上移动一只乌龟(在logo中),就没有办法知道它们将生成什么形状,或者该形状是否在不运行它们的情况下终止。但是,如果在运行它们时,我注意到乌龟的路径已经穿过了它自己,我可以向用户指示“啊哈!乌龟已经穿过了它的路径!”因此我知道乌龟已经到达了它以前到达过的位置——如果路径是一个通过计算图的节点而形成的回路,那么这就告诉我们处于循环中。然而,海龟也可以进入,例如,一个膨胀的螺旋。它永远不会终止,但也永远不会跨越它先前的道路

因此,由于黑洞的使用,出于多种原因,我们对评估遵循的标记“路径”有了一些概念。如果路径自身交叉,我们可以判断并抛出异常。然而,有一百万种方式使事情发生分歧,而不涉及路径交叉本身。在这些情况下,我们无法判断,也不会抛出异常


有关当前黑洞实现的超级极客技术细节,请参阅最近Haskell Implementors研讨会上Simon Marlow的演讲,“在多核上安排惰性评估”。

感谢bdonlan。我只是更新了一下我的问题。那么,试图检测无限循环背后的原因是什么?为什么在编译时没有警告的情况下运行时会出现异常?用错误替换这些东西的常用术语是“黑洞”。此外,例如cf,它基本上做了相同的事情。虽然GHC可能确实检测到简单的
let loop=loop in…
构造,但问题中的示例实际上不是使用“其他控制流分析”检测到的,而是在运行时通过“blackholing”机制检测到的。