Haskell的严格点是什么?


90

我们都知道(或应该知道)Haskell默认是懒惰的。在必须评估之前,不会评估任何内容。那么什么时候必须对某些东西进行评估?在某些方面,Haskell必须严格。我称这些为“严格点”,尽管这个特定术语没有我想象的那么广泛。据我说:

Haskell中的减少(或评估) 在严格点进行。

所以,问题是:什么,准确地说,是Haskell的严格点?我的直觉说mainseq/爆炸模式,模式匹配以及IO通过执行的任何操作main都是主要的严格要求,但是我真的不知道为什么知道这一点。

(另外,如果他们不叫“严点”,什么他们叫什么名字?)

我想一个很好的答案将包括有关WHNF的一些讨论。我也想像它可能会影响lambda演算。


编辑:关于此问题的其他想法。

正如我对这个问题的思考一样,我认为在严格性点的定义中添加一些内容会更加清晰。严格点可以具有变化的上下文和变化的深度(或严格性)。回到我的定义“ Haskell的减少仅在严格性点发生”,让我们在该定义中添加以下条款:“严格性点仅在评估或减少其周围上下文时触发。”

因此,让我尝试让您开始我想要的答案。main是严格的一点。它被专门指定为上下文的主要严格要点:程序。在main评估程序(的上下文)时,将激活main的严格点。Main的深度最大:必须对其进行充分评估。Main通常由IO操作组成,这些操作也是严格意义上的要点,其上下文是main

现在您尝试:seq用这些术语讨论和模式匹配。解释功能应用的细微差别:它有多严格?怎么不行 那deepseqletcase陈述?unsafePerformIODebug.Trace?顶级定义?严格的数据类型?爆炸图案?等等,仅用seq或模式匹配就可以描述其中的多少项?


10
您的直观列表可能不是很正交。我怀疑seq和模式匹配就足够了,其余的都是根据这些定义的。例如,我认为模式匹配可确保IO动作的严格限制。
CA McCann

诸如+内置数字类型之类的基元也强制严格性,我认为对纯FFI调用也是如此。
hammar 2011年

4
这里似乎混淆了两个概念。模式匹配以及seq和bang模式是使表达式的子表达式变得严格的方式-也就是说,如果计算顶部表达式,则子表达式也是如此。另一方面,主要执行的IO动作是评估开始的方式。这些是不同的东西,将它们包括在同一列表中是一种类型错误。
克里斯·史密斯

@ChrisSmith我不想混淆这两种不同的情况;如果有什么需要,我要求进一步澄清它们如何相互作用。严格性以某种方式发生,这两种情况都很重要,尽管有所不同,但严格性部分“正在发生”。(和@ monadic:ಠ_ಠ)
Dan Burton

如果您想/需要空间来讨论此问题的各个方面,而又不尝试给出完整的答案,请允许我建议利用我在/ r / haskell帖子中针对此问题的评论
Dan Burton

Answers:


46

一个不错的起点是理解本文:“惰性评估的自然语义学”(Launchbury)。这将告诉您何时使用类似于GHC Core的小语言评估表达式。然后剩下的问题是如何将完整的Haskell映射到Core,而大多数翻译是由Haskell报告本身给出的。在GHC中,我们称此过程为“去糖精”,因为它除去了语法糖。

嗯,这还不是全部,因为GHC包含了在调试和代码生成之间进行的一系列优化,而且许多这样的转换将重新安排Core,以便在不同的时间进行评估(特别是严格性分析将导致对事物进行评估)之前)。因此,要真正了解您的 程序将如何进行评估,您需要查看GHC产生的Core。

也许这个答案对您来说似乎有点抽象(我没有特别提到爆炸模式或序列),但是您要求的是精确的东西,这是我们能做到的最好的。


18
我一直觉得这很有趣,因为在GHC所谓的“减糖”中,被删除的语法糖包括Haskell语言本身实际语法 ……这似乎暗示着GHC实际上是GHC的优化编译器。核心语言,碰巧还包括一个非常复杂的前端,用于将Haskell转换为Core。:]
CA McCann

我记得,类型系统并不能精确地处理……特别是但不仅限于将类型类转换为显式词典方面。据我所知,所有最新的TF / GADT内容都使差距进一步扩大。
sclv 2011年


20

我可能会重述这个问题,因为Haskell在什么情况下会评估表达式?(也许坚持“弱头正常形态”。)

首先,我们可以这样指定:

  • 执行IO操作将评估它们“需要”的任何表达式。(因此,您需要知道IO操作是否已执行,例如它的名称是main,还是从main调用,并且您需要知道该操作需要什么。)
  • 正在评估的表达式(嘿,这是一个递归定义!)将评估它需要的任何表达式。

在您的直观列表中,主要操作和IO操作属于第一类,而seq和模式匹配属于第二类。但是我认为第一类更符合您的“严格点”概念,因为实际上这就是我们使Haskell中的评估成为用户可观察到的影响的方式。

由于Haskell是一门大语言,因此具体提供所有细节是一项艰巨的任务。这也很微妙,因为Concurrent Haskell可能会进行推测性评估,即使最终我们最终没有使用结果:这是引起评估的第三类事物。第二类是经过充分研究的:您想看看所涉及功能的严格性。第一类也可以被认为是一种“严格性”,尽管这有点儿狡猾,因为evaluate xseq x $ return ()实际上是不同的东西!如果为IO monad提供某种语义(RealWorld#可以在简单情况下显式传递令牌),则可以正确地对待它,但是我不知道这种分层严格性分析的名称是否一般。


17

C具有序列点的概念,这是对特定操作的保证,一个操作数将在另一个操作数之前进行评估。我认为这是最接近的现有概念,但是本质上等效的术语“ 严格性点”(或可能是“ 力点”)更符合Haskell的思想。

实际上,Haskell并不是纯粹的惰性语言:例如,模式匹配通常是严格的(因此,尝试模式匹配会迫使评估至少发生得足够远以接受或拒绝匹配。

程序员也可以使用seq原语来强制表达式求值,而不管是否将使用结果。

$!用定义seq

懒惰与非严格

所以你在想!/$!seq基本上是正确的,但模式匹配受到微妙的规则。当然,您总是可以使用~强制进行惰性模式匹配。该文章的有趣之处在于:

严格性分析器还会查找外部表达式始终需要子表达式的情况,并将其转换为热切的求值。之所以可以这样做,是因为语义(就“底部”而言)不变。

让我们继续深入研究,看看GHC进行的优化文档:

严格性分析是GHC在编译时尝试确定“始终需要”哪些数据的过程。然后,GHC可以构建代码以仅计算此类数据,而不是正常的(较高的开销)过程来存储计算并在以后执行。

GHC优化:严格性分析

换句话说,可以在任何地方生成严格的代码来进行优化,因为在总是需要数据(和/或只能使用一次)时,创建thunk不必要地昂贵。

…无法对该值进行更多评估;据说是正常形式。如果我们处于任何中间步骤中,以便我们至少对某个值执行了一些评估,则该值处于弱头正常形式(WHNF)。(还有一种“头部范式”,但在Haskell中未使用。)完全评估WHNF中的某些内容会将其简化为范式……

维基百科Haskell:懒惰

(如果头部位置1中没有beta-redex,则该术语为头部正常形式。如果redex仅在非redexex 2的lambda抽象器之前,则它是头部redex 。)因此,当您开始强制重击时,您正在WHNF工作;当没有多余的重击被迫时,您就处于正常状态。另一个有趣的地方:

…如果在某个时候我们需要将z打印出来,我们需要对其进行全面评估…

这自然意味着,事实上,任何IO行动从执行main 力的评价,这应该是显而易见的考虑Haskell程序做,其实做的事情。需要按照定义的顺序进行操作的任何内容都main必须为正常形式,因此必须经过严格评估。

CA麦肯这样做是正确的意见,虽然:这是特别的唯一的事情main是,main被定义为特殊的; 构造函数上的模式匹配足以确保IOmonad 施加的顺序。在这方面seq,模式匹配是基础。


4
实际上,“如果在某个时候我们需要将z打印出来给用户,我们需要对其进行全面评估”这句话并不是完全正确的。它Show与要打印的值的实例一样严格。
nominolo 2011年

10

Haskell是AFAIK,不是纯惰性语言,而是非严格语言。这意味着它不一定在最后可能的时刻评估条件。

可以在以下位置找到Haskell的“懒惰”模型的良好来源:http : //en.wikibooks.org/wiki/Haskell/Laziness

基本上,重要的是要了解重击和弱首部正常形式WHNF之间的区别。

我的理解是,与命令式语言相比,haskell将计算推后了一步。这意味着在没有“ seq”和“ bang”模式的情况下,最终将是某种副作用,它迫使对thunk进行评估,这又可能导致先前的评估(真正的懒惰)。

由于这将导致严重的空间泄漏,因此编译器会弄清楚如何以及何时评估内存块的节省空间。然后,程序员可以通过提供严格性注释(en.wikibooks.org/wiki/Haskell/Strictness,www.haskell.org/haskellwiki/Performance/Strictness)来支持此过程,以进一步减少嵌套thunk的形式使用空间。

我不是haskell的操作语义学专家,因此我只将链接保留为资源。

一些其他资源:

http://www.haskell.org/haskellwiki/Performance/Laziness

http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation


6

懒惰并不意味着什么也不做。只要您的程序模式与case表达式匹配,它就会对某些内容进行求值-无论如何都足够。否则,它无法确定要使用哪个RHS。在您的代码中看不到任何case表达式?不用担心,编译器会将您的代码转换为Haskell的精简形式,在这些形式中很难避免使用它们。

对于初学者来说,基本的经验法则let是懒惰,case而不是懒惰。


2
请注意,尽管case始终在GHC Core中强制执行评估,但在常规的Haskell中却不这样做。例如,尝试case undefined of _ -> 42
hammar 2011年

2
case在GHC核心评估其参数WHNF,而case在Haskell评估其参数根据需要尽可能多的选择适当的分支。在hammar的示例中,这一点儿都没有,但是在中case 1:undefined of x:y:z -> 42,它的评估要比WHNF的评估要深。
马克斯

而且也 case something of (y,x) -> (x,y)根本不需要评估something。所有产品类型都是如此。
Ingo

@Ingo-不正确。 something需要评估为WHNF才能到达元组构造函数。
约翰L

约翰-为什么?我们知道它必须是一个元组,那么对其进行评估的重点是什么?如果x和y绑定到评估元组并提取适当的插槽的代码,就足够了,只要它们本身需要,就足够了。
Ingo

4

对于业力而言,这不是一个完整的答案,但这只是一个难题-就涉及语义而言,请记住,有多种评估策略可提供相同的语义。这里一个很好的例子-该项目还谈到了我们通常对Haskell语义的看法-Eager Haskell项目,该项目从根本上改变了评估策略,同时保持了相同的语义:http : //csg.csail.mit.edu/ pubs / haskell.html


2

Glasgow Haskell编译器将您的代码翻译成类似于Lambda演算的语言,称为core。用这种语言,每当您通过case-statement进行模式匹配时,都会对其进行评估。因此,如果调用一个函数,则将评估最外层的构造函数及其仅有的构造函数(如果没有强制字段)。其他东西都装在罐头里。(let绑定通过绑定引入)。

当然,这并不是真实语言中发生的事情。编译器以一种非常复杂的方式将Haskell转换为Core,从而使尽可能多的事情变得懒惰,而总是需要的一切变得懒惰。此外,还有一些严格的未装箱值和元组。

如果您尝试手工评估一个函数,则基本上可以认为:

  • 尝试评估返回值的最外层构造函数。
  • 如果需要其他任何方法来获得结果(但仅在确实需要时),也将进行评估。顺序无关紧要。
  • 如果是IO,则必须评估从第一个到最后一个语句的所有语句的结果。这有点复杂,因为IO monad会执行一些技巧以强制按特定顺序进行评估。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.