什么是Haskell的严格要点?

时间:2011-09-20 19:34:46

标签: haskell language-design lazy-evaluation strictness

我们都知道(或应该知道)Haskell默认是懒惰的。在必须对其进行评估之前,不评估任何内容。那么什么时候必须评估一下? Haskell必须严格要点。我把这些称为“严格点”,虽然这个术语并不像我想象的那么广泛。据我说:

  

Haskell 中的减少(或评估)仅在严格点发生。

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

(另外,如果他们没有被称为“严格点”,那么 他们叫什么?)

我想一个好的答案将包括一些关于WHNF的讨论等等。我也想象它可能触及lambda演算。


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

正如我在这个问题上的反思,我认为在严格点的定义中添加一些东西会更清楚。严格点可以具有不同的上下文和不同的深度(或严格性)。回到我的定义“Haskell的减少只发生在严格点”,让我们在这个定义中添加这个子句:“只有在评估或减少其周围环境时才会触发严格点。”

所以,让我试着让你开始我想要的那种答案。 main是一个严格的观点。它被特别指定为其上下文的主要严格点:程序。当评估程序(main的上下文)时,激活main的严格点。主要深度是最大的:必须进行全面评估。 Main通常由IO操作组成,这些操作也是严格点,其上下文为main

现在您尝试:在这些条款中讨论seq和模式匹配。解释功能应用的细微差别:它是如何严格的?怎么回事?那么deepseq呢? letcase语句? unsafePerformIODebug.Trace?顶级定义?严格的数据类型?邦模式?等等。这些项目中有多少可以用seq或模式匹配来描述?

8 个答案:

答案 0 :(得分:45)

一个好的起点是理解这篇论文:A Natural Semantics for Lazy Evalution(Launchbury)。这将告诉你什么时候表达式被评估为类似于GHC的核心的小语言。然后剩下的问题是如何将完整的Haskell映射到Core,并且大部分翻译是由Haskell报告本身给出的。在GHC中,我们称这个过程为“desugaring”,因为它会消除语法糖。

嗯,这不是整个故事,因为GHC在desugaring和代码生成之间包含了大量的优化,并且许多这些转换将重新排列Core以便在不同时间对事物进行评估(特别是严格性分析会导致事情待评估)。所以要真正了解你的 程序将被评估,你需要看看GHC制作的核心。

也许这个答案对你来说有点抽象(我没有特别提到爆炸模式或seq),但是你要求提供精确的东西,这是我们能做的最好的事情。 / p>

答案 1 :(得分:20)

我可能会重新提出这个问题,在什么情况下Haskell会评估一个表达式?(也许是对“弱头正常形式”。)

首先,我们可以如下指定:

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

从直观列表中,主要和IO操作属于第一类,seq和模式匹配属于第二类。但我认为第一类更符合您对“严格点”的看法,因为实际上我们在Haskell中的评估是如何使用户成为可观察的效果。

特别提供所有细节是一项艰巨的任务,因为Haskell是一种大型语言。它也非常微妙,因为Concurrent Haskell可以推测性地评估事物,即使我们最终没有使用结果:这是导致评估的第三类事物。第二类是非常好的研究:你想看看所涉及的函数的严格性。第一类也可以被认为是一种“严格”,虽然这有点狡猾,因为evaluate xseq x $ return ()实际上是不同的东西!如果你给IO monad提供某种语义(明确传递RealWorld#令牌适用于简单的情况),你可以正确对待它,但我不知道是否有这种分层严格性分析的名称一般

答案 2 :(得分:17)

C具有sequence points的概念,它保证了一个操作数将在另一个操作数之前进行评估的特定操作。我认为这是最接近的现有概念,但基本等同的术语严格点(或可能力点)更符合Haskell思想。

  

在实践中,Haskell并不是一种纯粹的懒惰语言:例如,模式匹配通常是严格的(因此,尝试模式匹配会迫使评估发生至少足以接受或拒绝匹配。

     

...

     

程序员也可以使用seq原语强制表达式进行评估,无论结果是否会被使用。

     

$!是根据seq定义的。

     

- Lazy vs. non-strict

因此,您对! / $!seq的看法基本上是正确的,但模式匹配受制于更微妙的规则。当然,您总是可以使用~来强制进行惰性模式匹配。同一篇文章中有趣的一点:

  

严格性分析器还查找外部表达式总是需要子表达式并将其转换为急切评估的情况。它可以这样做,因为语义(就“底部”而言)不会改变。

让我们继续沿着兔子洞走下去,看看GHC进行优化的文档:

  

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

     

- GHC Optimisations: Strictness Analysis

换句话说,可以在任何地方生成严格的代码作为优化,因为当始终需要数据(和/或可能只使用一次)时,创建thunk会不必要地花费。

  

......不再对该值进行评估;它被认为是正常形式。如果我们处于任何中间步骤,以便我们至少对某个值进行了一些评估,则它处于弱头正常形式(WHNF)。 (还有一个'头部正常形式',但它没有在Haskell中使用。)在WHNF中全面评估某些内容会将其缩小为正常形式......

     

- Wikibooks Haskell: Laziness

(如果头部位置 1 中没有beta-redex,则术语为头部正常形式。如果是,则redex是头部重新索引仅在非重新索引 2 的lambda抽象之前。)所以当你开始强迫thunk时,你在WHNF工作;当没有更多的力量留给你时,你就处于正常状态。另一个有趣的观点:

  

...如果在某些时候我们需要将z打印给用户,我们需要对其进行全面评估......

这自然意味着,实际上,从IO 执行的任何main操作都强制进行评估,考虑到Haskell程序确实做了事情,这应该是显而易见的。任何需要通过main中定义的序列的东西必须处于正常状态,因此需要严格评估。

℃。 A. McCann在评论中说得对:main唯一特别之处在于main被定义为特殊;构造函数上的模式匹配足以确保IO monad强加的序列。在这方面,只有seq和模式匹配才是基础。

答案 3 :(得分:9)

Haskell是AFAIK,不是一种纯粹的懒惰语言,而是一种非严格的语言。这意味着它不一定在最后一刻评估术语。

haskell的“懒惰”模型的一个很好的来源可以在这里找到:http://en.wikibooks.org/wiki/Haskell/Laziness

基本上,理解thunk和弱标头正常形式WHNF之间的区别非常重要。

我的理解是,与命令式语言相比,haskell通过向后拉动计算。这意味着,在没有“seq”和爆炸模式的情况下,最终会出现某种副作用,迫使对thunk的评估,这可能会导致先前的评估(真正的懒惰)。

由于这会导致可怕的空间泄漏,然后编译器会计算出如何以及何时提前评估thunks以节省空间。然后,程序员可以通过严格注释(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

答案 4 :(得分:6)

懒惰并不意味着什么都不做。每当你的程序模式与case表达式匹配时,它就会评估一些东西 - 无论如何都足够了。否则无法确定使用哪种RHS。在代码中看不到任何case表达式?不用担心,编译器正在将您的代码转换为Haskell的简化形式,而这些形式很难避免使用。

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

答案 5 :(得分:4)

这不是一个针对业力的完整答案,而只是一个难题 - 在某种程度上这是关于语义的,请记住,有多种评估策略可以提供相同的语义学。这里有一个很好的例子 - 该项目也说明了我们通常如何看待Haskell语义 - 是Eager Haskell项目,它在维护相同的语义时彻底改变了评估策略:http://csg.csail.mit.edu/pubs/haskell.html

答案 6 :(得分:2)

Glasgow Haskell编译器将您的代码转换为类似Lambda-calculus的语言,称为 core 。在这种语言中,只要您通过case语句对模式进行匹配,就会对某些内容进行评估。因此,如果调用一个函数,那么将评估最外层的构造函数以及它(如果没有强制字段)。其他任何东西都是在thunk中罐装。 (Thunks由let绑定引入)。

当然,这并不是真正的语言。编译器以非常复杂的方式将Haskell转换为Core,使得尽可能多的东西变得懒惰以及任何总是需要懒惰的东西。此外,还有一些始终严格的未装箱值和元组。

如果您尝试手动评估某个功能,您基本上可以认为:

  • 尝试评估返回的最外层构造函数。
  • 如果需要其他任何东西来获得结果(但只有在真正需要时)才会被评估。订单无关紧要。
  • 如果是IO,您必须评估从第一个到最后一个语句的所有语句的结果。这有点复杂,因为IO monad做了一些技巧来强制按特定顺序进行评估。

答案 7 :(得分:0)

我们都知道(或应该知道)Haskell默认是懒惰的。在必须评估之前,不会评估任何东西。

否。

Haskell不是一种懒惰的语言

Haskell是一种语言,其中评估顺序无关紧要,因为它没有副作用。

评估顺序并不重要,因为该语言允许无限循环。如果您不小心,则可能会陷入死路,在此情况下,如果不同的评估顺序会导致在有限时间内终止,则您将永远评估子表达式。所以说的更准确:

  • 如果有任何评估顺序终止,则Haskell实现必须以终止方式评估程序。只有每个可能的评估订单都无法终止,实施才能终止。

这仍然使实现在评估程序方面具有很大的自由度。

Haskell程序是单个表达式,即let { 所有顶级绑定 } in Main.main。评估可以理解为一系列减少(小)步骤的步骤,这些步骤可以改变表达式(代表执行程序的当前状态)。

您可以将减少步骤分为两类:证明是必需的(证明是任何终止序列的一部分),而不是不必要的。您可以将可证明必要的减少量含糊地划分为两个子类别:“明显”必要的减少和需要一些简单分析以证明必要的减少。

仅执行明显必要的减少操作,即所谓的“惰性评估”。我不知道是否曾经有过纯粹懒惰的Haskell评估实现。拥抱可能是其中之一。 GHC绝对不是。

GHC在编译时执行不必要的缩减步骤;例如,即使无法证明将使用结果,也会将1+2::Int替换为3::Int

在某些情况下,GHC可能还会在运行时执行不必要的减少操作。例如,在生成代码以评估f (x+y)时,如果xy的类型为Int,并且它们的值在运行时是已知的,但是f无法证明使用其参数,因此没有理由在调用x+y之前不计算f。它使用较少的堆空间和较少的代码空间,并且即使不使用该参数也可能更快。但是,我不知道GHC是否真正抓住了这些优化机会。

GHC肯定会在运行时执行评估步骤,只有通过相当复杂的跨模块分析才能证明评估步骤是必要的。这是非常普遍的现象,可能代表了对现实计划的大部分评估。惰性评估是最后手段的后备评估策略;这不是通常发生的事情。

GHC的一个"optimistic evaluation"分支在运行时进行了更多的推测性评估。它被放弃是因为它的复杂性和持续的维护负担,而不是因为它表现不佳。如果Haskell像Python或C ++一样流行,那么我相信会有财力雄厚的公司维护具有更复杂的运行时评估策略的实现。非惰性评估不会改变语言,只是工程上的挑战。

减少是由顶级I / O驱动的,除此之外没有其他

您可以通过特殊的副作用减少规则来模拟与外界的交互,例如:“如果当前程序的格式为getChar >>= <expr>,则从标准输入中获取字符并将程序简化为{ {1}}应用于您得到的字符。”

运行时系统的整个目标是评估程序,直到它具有这些副作用形式之一,然后产生副作用,然后重复执行,直到程序具有某种暗示终止的形式,例如{{1 }}。

关于何时减少什么没有其他规则。关于什么可以减少到什么只有规则。

例如,<expr>表达式的唯一规则是return ()可以简化为ifif True then <expr1> else <expr2>可以简化为<expr1>和{ {1}}(其中if False then <expr1> else <expr2>是一个例外值)可以减小为例外值。

如果表示程序当前状态的表达式是<expr2>表达式,则您别无选择,只能对条件进行归约直到if <exc> then <expr1> else <expr2><exc>或{{1 }},因为这是摆脱if表达式并希望达到与I / O规则之一匹配的状态的唯一途径。但是语言规范并没有告诉您要用这么多字做到这一点。

这些类型的隐式排序约束是可以“强制”执行评估的唯一方法。这是初学者经常感到困惑的原因。例如,人们有时尝试通过写True而不是False来使<exc>更加严格。这是行不通的,没有什么比它行得通的了,因为没有表达式可以使自己求值。评估只能“来自上方”。 if在这方面并不特别。

还原反应无处不在

Haskell中的减少(或评估)仅在严格点进行。 [...]我的直觉说,main,seq / bang模式,模式匹配以及通过main执行的任何IO动作都是主要的严格要求[...]。

我看不出如何理解这一说法。程序的每个部分都有一定的含义,并且该含义由归约规则定义,因此归约发生在各处。

要简化功能应用程序foldl,必须评估foldl (\x y -> x `seq` x+y),直到其具有类似foldl (+)seq的形式或与规则匹配的其他形式。但是出于某种原因,函数应用程序通常不会出现在据称“强制求值”的表达式列表中,而<expr1> <expr2>总是这样。

您可以在Haskell Wiki的引文中看到这种误解,该引文在另一个答案中找到:

实际上,Haskell并不是纯粹的惰性语言:例如模式匹配通常是严格的

我不明白对于写这本书的人,什么可以被视为“纯粹的惰性语言”,除了,也许是每个程序都挂起的语言,因为运行时从不执行任何操作。如果模式匹配是您语言的功能,那么您必须在某个时候进行实际操作。为此,您必须对检查进行充分评估,以确定它是否与模式匹配。这是原则上匹配模式的最懒惰的方式。

带有

<expr1>前缀的模式通常被程序员称为“惰性”,但是语言规范称它们为“不可辩驳的”。它们的定义属性是它们始终匹配。因为它们总是匹配,所以您不必评估检查器来确定它们是否匹配,因此懒惰的实现不会。常规模式和不可辩驳模式之间的区别在于它们匹配哪些表达式,而不是您应该使用哪种评估策略。该规范对评估策略一无所知。


(\x -> <expr1'>)是严格点。它被专门指定为上下文的主要严格要点:程序。在评估程序((getChar >>=)的上下文)时,将激活main的严格点。 [...] Main通常由IO操作组成,它们也是严格意义上的要点,其上下文为case

我不相信任何这些都有任何意义。

电源的深度最大:必须对其进行充分评估。

否,~仅需要“浅”评估,以使I / O操作出现在顶层。 main是整个程序,并且由于并非所有代码都与每次运行相关(通常),因此不会在每次运行时都对该程序进行完全评估。

用这些术语讨论main和模式匹配。

我已经讨论过模式匹配。 main可以由与main和应用类似的规则定义:例如,main简化为seq。这与seq和应用程序执行“强制评估”的方式相同。 WHNF只是这些表达“强制评估”所要表达的名称。

解释功能应用的细微差别:它有多严格?怎么了?

它的左边表达很严格,就像case的严格要求一样。替换后的函数主体也很严格,就像(\x -> <expr1>) `seq` <expr2>替换后所选替代项的RHS一样严格。

<expr2>呢?

这只是一个库函数,而不是内置函数。

顺便说一句,case在语义上很奇怪。它只需要一个论点。我认为,发明它的人只是盲目地复制了case,却不了解为什么case需要两个论点。我认为deepseq的名称和规范可以证明,即使在有经验的Haskell程序员中,对Haskell评估的理解不足也是很普遍的。

deepseqseq语句?

我谈到了seq。经过deepseq的调试和类型检查之后,它只是一种以树形式编写任意表达式图的方法。 Here's a paper about it

let

在一定程度上可以由缩减规则定义。例如,case减少为case,仅在顶层,let减少为unsafePerformIO

这不会做任何记录。您可以尝试通过重写每个case unsafePerformIO <expr> of <alts>表达式来显式地对其自身进行记忆,并在某个位置创建关联的unsafePerformIO (<expr> >>= \x -> return (case x of <alts>))来模拟记忆。但是,您永远无法重现GHC的记忆行为,因为它取决于优化过程的不可预测的细节,而且甚至都不是类型安全的(如GHC文档中臭名昭著的多形unsafePerformIO <expr>示例所示)。

<expr>

unsafePerformIO只是IORef的简单包装。

顶级定义?

顶级变量绑定与嵌套IORef绑定相同。 Debug.TraceDebug.Trace.traceunsafePerformIO等都是完全不同的球类游戏。

严格的数据类型?爆炸图案?

let只是糖。