以下值/表达式/函数的thunk在Haskell堆中看起来是什么样的?
val = 5 -- is `val` a pointer to a box containing 5?
add x y = x + y
result = add 2 val
main = print $ result
如果考虑到它的懒惰评估模式,可以很好地了解这些在Haskell中是如何表示的。
答案 0 :(得分:60)
这不关你的事。严格执行编译器的详细信息。
是
对于Haskell程序本身,答案总是肯定的,但是出于性能原因,如果编译器发现它可以侥幸逃脱,那么编译器可以而且会做不同的事情。
例如,对于'''add x y = x + y''',编译器可能会生成适用于x和y的thunk的代码,并构造thunk作为结果。 但请考虑以下因素:
foo :: Int -> Int -> Int
foo x y = x * x + y * y
在这里,优化编译器将生成首先从其框中取出x和y的代码,然后执行 all 算法,然后将结果存储在一个框中。
本文描述了GHC如何从实现thunks的一种方式切换到实际上既简单又快速的方式: http://research.microsoft.com/en-us/um/people/simonpj/papers/eval-apply/
答案 1 :(得分:13)
通常,即使是Haskell中的原始值(例如Int和Float类型)也可以用thunks表示。这确实是非严格语义所要求的;请考虑以下片段:
bottom :: Int
bottom = div 1 0
如果检查底部的值,此定义将仅生成 ,但如果从未使用该值,则不会生成
。现在考虑添加功能:
add :: Int -> Int -> Int
add x y = x+y
一个朴素的add实现必须强制thunk为x,强制thunk为y,添加值并为结果创建(评估)thunk。与严格的函数语言(更不用说命令式语言)相比,这是算术的巨大开销。
然而,GHC等优化编译器可以避免这种开销;这是GHC如何翻译添加功能的简化视图:
add :: Int -> Int -> Int
add (I# x) (I# y) = case# (x +# y) of z -> I# z
在内部,像Int这样的基本类型被视为具有单个构造函数的数据类型。 Int#类型是整数的“原始”机器类型,+#是原始类型的原始添加。 原始类型的操作直接在位模式(例如寄存器)上实现 - 而不是thunks。然后将装箱和拆箱翻译为构造函数应用程序和模式匹配。
这种方法的优点(在这个简单的例子中不可见)是编译器通常能够内联这样的定义并删除中间装箱/拆箱操作,只留下最外层的操作。
答案 2 :(得分:7)
将每个值包装在thunk中是绝对正确的。但由于Haskell不严格,编译器可以选择何时评估thunk /表达式。特别是,如果表达式产生更好的代码,编译器可以选择在严格必要之前评估表达式。
优化Haskell编译器(GHC)执行 Strictness analysis 以确定将始终计算哪些值。
一开始,编译器必须假定,没有使用任何函数的参数。然后它遍历函数体并尝试查找1)已知其(至少某些)参数严格的函数应用程序,并且2)总是必须进行求值以计算函数的结果。
在您的示例中,我们的函数(+)
在两个参数中都是严格的。因此,编译器知道此时始终需要评估x
和y
。
现在它恰好发生了,表达式x+y
总是需要计算函数的结果,因此编译器可以存储add
和{{x
中函数y
严格的信息。 1}}。
add
*的生成代码因此将期望整数值作为参数而不是thunk。当涉及递归时(固定点问题),算法变得复杂得多,但基本思想保持不变。
另一个例子:
mkList x y =
if x then y : []
else []
此函数将x
以评估形式(作为布尔值)和y
作为thunk。需要在x
的每个可能的执行路径中计算表达式mkList
,因此我们可以让调用者对其进行评估。另一方面,表达式y
从未在任何对其参数严格的函数应用程序中使用。缺点函数:
从不查看y
它只是将其存储在列表中。因此y
需要作为thunk传递,以满足懒惰的Haskell语义。
mkList False undefined -- absolutely legal
*:add
当然是多态的,x
和y
的确切类型取决于实例化。
答案 3 :(得分:6)
简短回答:是的。
答案很长:
val = 5
这必须存储在thunk中,因为想象一下,如果我们在代码中写了 where (比如,在我们导入的库中或其他东西):
val = undefined
如果在我们的程序启动时必须对此进行评估,它会崩溃,对吧?如果我们实际上将这个值用于某些东西,那就是我们想要的,但是如果我们不使用它,它就不应该如此灾难性地影响我们的程序。
对于你的第二个例子,让我稍微改变一下:
div x y = x / y
这个值也必须存储在thunk中,因为想象一下这样的代码:
average list =
if null list
then 0
else div (sum list) (length list)
如果div
在这里是严格的,即使列表是null
(也就是空),它也会被评估,这意味着写这样的函数是行不通的,因为它不会当给出空列表时,有机会返回0
,即使这是我们在这种情况下想要的。
你的最后一个例子只是例1的变体,出于同样的原因它必须是懒惰的。
所有这一切都说,有可能强制编译器使某些值严格,但这超出了这个问题的范围。
答案 4 :(得分:4)
我认为其他人很好地回答了你的问题,但为了完整起见,我想补充说GHC为你提供了直接使用未装箱值的可能性。这就是Haskell Wiki says about it:
如果您真的非常渴望速度,并希望直接使用“原始位”。请参阅GHC Primitives以获取有关使用未装箱类型的一些信息。
然而,这应该是最后的手段,因为未装箱的类型和基元是不可移植的。幸运的是,通常没有必要使用显式的未装箱类型和原语,因为GHC的优化器可以通过内联它所知道的操作和取消装箱严格的函数参数来为你完成工作。严格和解压缩的构造函数字段也可以提供很多帮助。有时GHC需要一些帮助来生成正确的代码,因此您可能需要查看Core输出以查看您的调整是否实际上具有所需的效果。
使用未装箱的类型和原语可以说的一件事是,你知道你正在编写有效的代码,而不是依靠GHC的优化器来做正确的事情,并且受到GHC优化器的变化的影响。线。这对你来说很重要,在这种情况下去吧。
如上所述,它是不可移植的,因此您需要GHC语言扩展。有关他们的文档,请参阅here。