我们都知道(或应该知道)Haskell 默认是惰性的。在必须评估之前,不会评估任何内容。
不。
Haskell 不是一种懒惰的语言
Haskell 是一种语言,其中评估顺序无关紧要,因为没有副作用。
评估顺序无关紧要并不完全正确,因为该语言允许无限循环。如果您不小心,可能会陷入死胡同,当不同的评估顺序会导致在有限时间内终止时,您将永远评估子表达式。所以更准确的说法是:
- 如果有任何终止的评估顺序,Haskell 实现必须以终止的方式评估程序。只有当每个可能的评估顺序都未能终止时,执行才能终止。
这仍然使实现在如何评估程序方面拥有巨大的自由。
Haskell 程序是一个单一的表达式,即let {
所有顶级绑定} in Main.main
。评估可以理解为一系列减少(小)步骤,这些步骤改变了表达式(表示执行程序的当前状态)。
您可以将归约步骤分为两类:那些被证明是必要的(可证明是任何终止归约序列的一部分),以及那些不是。您可以模糊地将可证明必要的减少分为两个子类别:那些“显然”必要的,以及需要一些非平凡分析来证明它们必要的那些。
仅执行明显必要的缩减是所谓的“惰性评估”。我不知道是否曾经存在过 Haskell 的纯粹惰性求值实现。拥抱可能就是其中之一。GHC 绝对不是。
GHC 在编译时执行一些可证明不必要的缩减步骤;例如,即使不能证明结果会被使用,它也会1+2::Int
替换为。3::Int
在某些情况下,GHC 还可能在运行时执行不可证明的必要缩减。例如,当生成代码来评估时f (x+y)
,如果x
和y
是类型Int
并且它们的值将在运行时知道,但f
不能证明使用它的参数,没有理由x+y
在调用之前不计算f
。它使用更少的堆空间和更少的代码空间,即使不使用参数也可能更快。但是,我不知道 GHC 是否真的抓住了这些优化机会。
GHC 肯定会在运行时执行评估步骤,这些步骤仅通过相当复杂的跨模块分析证明是必要的。这是非常普遍的,可能代表了对现实项目的大部分评估。惰性评估是最后的后备评估策略;这不是通常发生的事情。
GHC有一个“乐观评估”分支,它在运行时进行了更多的推测性评估。它被放弃是因为它的复杂性和持续的维护负担,而不是因为它表现不佳。如果 Haskell 像 Python 或 C++ 一样流行,那么我相信会有一些具有更复杂的运行时评估策略的实现,由财力雄厚的公司维护。非惰性求值不是对语言的改变,它只是一个工程挑战。
减少由顶级 I/O 驱动,仅此而已
您可以通过特殊的副作用缩减规则对与外部世界的交互进行建模,例如:“如果当前程序是形式getChar >>= <expr>
,则从标准输入中获取一个字符并将程序缩减为<expr>
应用于您获得的字符。”
运行时系统的整个目标是评估程序,直到它具有这些副作用形式之一,然后执行副作用,然后重复直到程序具有某种意味着终止的形式,例如return ()
.
关于什么时候减少什么没有其他规则。只有关于什么可以减少到什么的规则。
例如,if
表达式的唯一规则是if True then <expr1> else <expr2>
可以简化为<expr1>
,if False then <expr1> else <expr2>
可以简化为<expr2>
,并且if <exc> then <expr1> else <expr2>
,其中<exc>
是异常值,可以简化为异常值。
如果表示程序当前状态的表达式是一个if
表达式,那么您别无选择,只能对条件执行约简,直到它是True
or False
or ,因为这是您摆脱表达式并有希望达到<exc>
的唯一方法if
匹配 I/O 规则之一的状态。但是语言规范并没有用很多话告诉你这样做。
这些类型的隐式排序约束是评估可以“强制”发生的唯一方式。对于初学者来说,这是一个常见的困惑来源。例如,人们有时会尝试foldl
通过写作foldl (\x y -> x `seq` x+y)
而不是foldl (+)
. 这是行不通的,而且没有任何类似的东西可以工作,因为没有表达式可以让自己进行评估。评价只能“自上而下”。seq
在这方面没有任何特别之处。
减少无处不在
Haskell 中的缩减(或评估)只发生在严格点上。[...] 我的直觉认为 main、seq / bang 模式、模式匹配以及通过 main 执行的任何 IO 操作都是主要的严格点 [...]。
我不明白如何理解这种说法。程序的每个部分都有一些含义,而那个含义是由归约规则定义的,所以归约无处不在。
要减少函数应用程序<expr1> <expr2>
,您必须进行评估<expr1>
,直到它具有类似(\x -> <expr1'>)
or(getChar >>=)
或其他与规则匹配的形式。但是由于某种原因,函数应用程序并不倾向于出现在据称“强制评估”的表达式列表中,而case
总是出现。
您可以在 Haskell wiki 的引用中看到这种误解,在另一个答案中找到:
实际上,Haskell 并不是一种纯粹的惰性语言:例如,模式匹配通常是严格的
我不明白对于编写该语言的人来说,什么可以称为“纯粹的惰性语言”,也许除了一种语言,每个程序都挂起,因为运行时从不做任何事情。如果模式匹配是你的语言的一个特性,那么你必须在某个时候真正做到这一点。要做到这一点,您必须对受检者进行足够的评估,以确定它是否与模式匹配。这是匹配原则上可能的模式的最懒惰的方式。
~
-前缀模式通常被程序员称为“懒惰”,但语言规范称它们为“无可辩驳的”。它们的定义属性是它们总是匹配的。因为它们总是匹配,所以您不必评估检查者来确定它们是否匹配,因此惰性实现不会。常规模式和无可辩驳的模式之间的区别在于它们匹配的表达式,而不是您应该使用的评估策略。该规范没有说明评估策略。
main
是一个严格点。它被特别指定为其上下文的主要严格点:程序。当程序(main
的上下文)被评估时, main 的严格点被激活。[...] Main 通常由 IO 动作组成,这些动作也是严格点,其上下文为main
.
我不相信这些有任何意义。
Main 的深度最大:必须对其进行全面评估。
不,main
只需要“浅层地”评估,以使 I/O 操作出现在顶层。main
是整个程序,并且在每次运行时都不会完全评估程序,因为并非所有代码都与每次运行相关(通常)。
在这些术语中讨论seq
和模式匹配。
我已经讨论过模式匹配。seq
可以通过类似于case
和应用程序的规则来定义:例如,(\x -> <expr1>) `seq` <expr2>
归约到<expr2>
. 这以case
与应用程序相同的方式“强制评估”。WHNF 只是这些表达式“强制评估”的名称。
解释函数应用的细微差别:它有多严格?怎么不是?
它的左表达式很严格,就像case
它的受检者很严格一样。替换后的函数体中也是严格的,就像case
替换后所选替代的 RHS 中的严格一样。
怎么样deepseq
?
它只是一个库函数,而不是内置函数。
顺便说一句,deepseq
在语义上很奇怪。它应该只需要一个参数。我认为发明它的人只是盲目地复制seq
,不明白为什么seq
需要两个论据。我将deepseq
的名称和规范视为证据,即使在有经验的 Haskell 程序员中,对 Haskell 评估的理解也很普遍。
let
和case
声明?
我谈到了case
。let
,在脱糖和类型检查之后,只是一种以树形式编写任意表达式图的方法。这是一篇关于它的论文。
unsafePerformIO
?
在一定程度上,它可以通过归约规则来定义。例如,case unsafePerformIO <expr> of <alts>
归约为unsafePerformIO (<expr> >>= \x -> return (case x of <alts>))
,并且仅在顶层,unsafePerformIO <expr>
归约为<expr>
。
这不会做任何记忆。您可以尝试通过重写每个unsafePerformIO
表达式来显式地记忆自己,并在IORef
某处创建关联的 s... 来模拟记忆。但是你永远无法重现 GHC 的记忆行为,因为它依赖于优化过程的不可预知的细节,而且它甚至不是类型安全的(如IORef
GHC 文档中臭名昭著的多态示例所示)。
Debug.Trace
?
Debug.Trace.trace
只是一个简单的包装unsafePerformIO
。
顶级定义?
顶级变量绑定与嵌套let
绑定相同。data
, class
, import
, 等等是完全不同的球类游戏。
严格的数据类型?爆炸模式?
只是糖seq
。