在Haskell声明中,感叹号是什么意思?


261

当我尝试使用真实的项目来驱动Haskell时,遇到了以下定义。我不明白每个论点前面的感叹号是什么意思,我的书似乎也没有提到它。

data MidiMessage = MidiMessage !Int !MidiMessage

13
我怀疑这可能是一个非常普遍的问题。我确实清楚地记得自己回想什么时候曾经想过完全相同的事情。
cjs

Answers:


313

这是一个严格的声明。基本上,这意味着在创建数据结构值时,必须将其评估为所谓的“弱头范式”。让我们看一个例子,以便我们可以看到这意味着什么:

data Foo = Foo Int Int !Int !(Maybe Int)

f = Foo (2+2) (3+3) (4+4) (Just (5+5))

f上面的函数在被求值时将返回“ thunk”:即,执行以确定其值的代码。到那时,Foo甚至还不存在,仅存在代码。

但是在某些时候,可能有人会尝试通过模式匹配来查看其中的内容:

case f of
     Foo 0 _ _ _ -> "first arg is zero"
     _           -> "first arge is something else"

这将执行足够的代码来完成所需的工作,仅此而已。因此它将创建具有四个参数的Foo(因为如果没有它,您将无法在其中查看内容)。首先,因为我们正在测试它,所以我们需要一直评估到4,直到我们发现它不匹配。

第二个不需要评估,因为我们没有测试它。因此,6我们不会将代码存储在该内存位置中,而是将其存储以用于以后的评估(3+3)。只有当有人看着它时,它才会变成6。

但是,第三个参数!在其前面,因此必须经过严格评估:(4+4)已执行并8存储在该内存位置。

第四个参数也被严格评估。但是,这里有些棘手:我们没有完全评估,而只是评估弱势的正常头部。这意味着我们要弄清楚是什么Nothing还是Just什么,然后存储它,但是我们走得更远。这意味着我们Just 10实际上不是存储而是Just (5+5)保留其中的杂物未被评估。要知道这一点很重要,尽管我认为这的所有含义都超出了此问题的范围。

如果启用BangPatterns语言扩展,则可以用相同的方式注释函数参数:

f x !y = x*y

f (1+1) (2+2)会归还笨拙的(1+1)*4


16
这非常有帮助。但是,我不禁要问,Haskell术语是否会使人们理解诸如“弱正常头型”,“严格”等术语变得更加复杂。如果我对您的理解正确,听起来就像!运算符只是意味着存储表达式的求值,而不是存储匿名块以供稍后求值。这是一个合理的解释吗?
戴维2009年

71
@David问题是评估价值有多远。弱头范式意味着:对其进行评估,直到到达最外面的构造函数为止。范式表示评估整个值,直到没有剩余未评估的成分为止。因为Haskell允许进行各种级别的评估深度,所以它具有丰富的术语来描述。您不会在只支持按值调用语义的语言中找到这些区别。
唐·斯图尔特

8
@David:我在这里写了更深入的解释:Haskell:什么是弱头范式?。尽管我没有提及爆炸模式或严格性注释,但它们等同于使用seq
hammar 2012年

1
可以肯定的是,我理解:懒惰是否仅与编译时参数未知有关?即,如果源代码确实具有语句(2 + 2),是否仍将其优化为4,而不是使用代码来添加已知数字?
Hi-Angel

1
Hi-Angel,这实际上取决于编译器要优化的程度。尽管我们用刘海来表达我们想将某些东西强制变弱为正常的头部形态,但是我们没有(也不需要,也不需要)说“请确保您尚未评估这个笨拙的东西”的方法。进一步。” 从语义的角度来看,给定一个沉重的“ n = 2 + 2”,您甚至无法确定是对n的任何特定引用现在正在评估还是先前已评估。
cjs 2015年

92

观察严格构造函数和非严格构造函数参数之间区别的一种简单方法是,当它们未定义时它们的行为。给定

data Foo = Foo Int !Int

first (Foo x _) = x
second (Foo _ y) = y

由于非严格参数不是由计算的second,因此传入undefined不会造成问题:

> second (Foo undefined 1)
1

但是严格的参数不能是undefined,即使我们不使用该值:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined

2
这比Curt Sampson的回答要好,因为它实际上解释了该!符号具有用户可观察的效果,而不是深入研究内部实现细节。
David Grayson

26

我相信这是严格的注释。

Haskell是一种纯粹且懒惰的函数式语言,但是有时懒惰的开销可能太多或浪费。因此,为了解决这个问题,您可以要求编译器完全评估函数的参数,而不是解析重击。

此页面上有更多信息:性能/严格度


嗯,您引用的该页面上的示例似乎在谈论!的用法。调用功能时。但这似乎与将其放入类型声明不同。我想念什么?
大卫

创建类型的实例也是一个表达式。给定提供的参数,您可以将类型构造函数视为返回指定类型的新实例的函数。
克里斯·

4
实际上,就所有意图和目的而言,类型构造函数都是函数。您可以部分应用它们,将它们传递给其他函数(例如,map Just [1,2,3]获取[Just 1,Just 2,Just 3]),依此类推。我发现考虑与他们进行模式匹配的能力以及完全不相关的功能会有所帮助。
cjs
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.