我认为像这样的表达式会导致Haskell永远评估。但GHCi和编译程序中的行为让我感到惊讶。
例如,在GHCi中,这些表达式被阻塞直到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>>
所以我的问题是:显然这些表达式被编译成与命令式语言中的循环或递归调用不同的东西。他们编译的是什么?这是处理右手边的0-arg函数的特殊规则,还是我不知道的更普遍的特殊情况?
[编辑]:
还有一个问题:如果这恰好是编译器的特殊处理,那么在无法检查所有无限循环时执行此操作的原因是什么? “熟悉”语言不关心while (true);
或int f() { return f();}
等情况,对吗?
非常感谢。
答案 0 :(得分:21)
GHC将Haskell实现为图形缩减机。将您的程序想象成一个图形,每个值都作为一个节点,并从它到每个值所依赖的值。除此之外,我们很懒,所以你真的只从一个节点开始 - 为了评估该节点,GHC必须“输入”它并将其打开到带参数的函数。然后它将函数调用替换为函数体,并尝试将其减少到足以使其成为头部正常形式等。
上面的内容很简洁,为了简洁起见,我肯定会删除一些必要的细节。
在任何情况下,当GHC输入一个值时,它通常会在评估节点时将其替换为黑洞(或者,根据您的术语,在关闭时减少)这有很多用途。首先,它会堵塞潜在的空间泄漏。如果节点引用了其他地方使用的值,则即使在评估节点时,黑洞也允许对该值进行垃圾收集。其次,这可以防止某些类型的重复工作,因为在多线程环境中,两个线程可能会尝试输入相同的值。黑洞将导致第二个线程阻塞而不是评估已经评估的值。最后,这恰好允许有限形式的循环检测,因为如果一个线程试图重新进入其自己的黑洞,我们可以抛出异常。
这是一个更隐喻的解释。如果我有一系列指令在屏幕周围移动一只乌龟(标识),那么没有一种方法可以判断它们将产生什么样的形状,或者这种形状是否在没有运行的情况下终止。但是,如果在运行它们时,我注意到乌龟的路径已经越过,我可以向用户表明“啊哈!乌龟穿过了它的路径!”所以我知道乌龟已经到达了之前的位置 - 如果路径是通过评估图形节点的电路,那么告诉我们我们处于循环中。然而,乌龟也可以进入,例如,膨胀螺旋。它永远不会终止,但它也永远不会超越它的先前路径。
因此,由于使用黑洞,出于多种原因,我们有一些评估所遵循的标记“路径”的概念。如果路径穿过自己,我们可以告诉并抛出异常。但是,有一百万种方法可以分散,而不涉及穿越自身的路径。在这些情况下,我们无法分辨,也不会抛出异常。
关于目前黑洞实施的超级技术细节,请参阅Simon Marlow在最近的Haskell实施者研讨会上的讲话,“{3}}底部的”安排多核的懒惰评估“。
答案 1 :(得分:7)
在一些有限的情况下,编译器可以确定这样的循环作为其他控制流分析的一部分存在,并且在那时将循环术语替换为抛出适当异常的代码。当然,这并不是在所有情况下都可以完成,而只是在一些更明显的情况下,它们自然而然地脱离了编译器正在进行的其他工作。
至于为什么Haskell比其他语言更经常发现这个:
_|_
(“底部”),用于表示错误的值。对自己严格的值 - 即,它们依赖于自己的计算值 - 是_|_
。 _|_
上的模式匹配结果可以是无限循环或异常;你的编译器在这里选择后者。