什么是弱头范式(WHNF)是什么意思?什么是头标准型(HNF)和范式(NF)是什么意思?
熟悉的seq函数将表达式计算为我们称之为head normal form(缩写为HNF)的表达式.它一旦到达最外面的构造函数("头部")就会停止.这与正常形式(NF)不同,其中表达式被完全评估.
您还将听到Haskell程序员引用弱头正常形式(WHNF).对于正常数据,弱头正常形式与头部正常形式相同.差异只出现在功能上,而且我们在这里无关紧要.
我已经阅读了一些资源和定义(Haskell Wiki和Haskell邮件列表和自由词典),但我没有得到它.有人可能举一个例子或提供外行定义吗?
我猜它会类似于:
WHNF = thunk : thunk
HNF = 0 : thunk
NF = 0 : 1 : 2 : 3 : []
Run Code Online (Sandbox Code Playgroud)
如何做seq
和($!)
与WHNF和HNF有关?
我还是很困惑.我知道有些答案会忽略HNF.通过阅读各种定义,似乎WHNF和HNF中的常规数据之间没有区别.但是,它似乎与功能有所区别.如果没有差异,为什么还seq
需要foldl'
?
另一个混淆点来自Haskell Wiki,它指出seq
减少到WHNF,并且对以下示例不做任何处理.然后他们说他们必须seq
用来强迫评估.那不是强迫它到HNF吗?
常见的新手堆栈溢出代码:
Run Code Online (Sandbox Code Playgroud)myAverage = uncurry (/) . foldl' (\(acc, len) x -> (acc+x, len+1)) (0,0)
了解seq和弱头正常形式(whnf)的人可以立即明白这里出了什么问题.(acc + x,len + 1)已经在whnf中,所以seq将值减少到whnf,对此无效.这段代码将像原始的foldl示例一样构建thunks,它们只是在元组内部.解决方案只是强制元组的组件,例如
Run Code Online (Sandbox Code Playgroud)myAverage …
我们都知道(或应该知道)Haskell默认是懒惰的.在必须对其进行评估之前,不评估任何内容.那么什么时候必须评估一下?Haskell必须严格要点.我把这些称为"严格点",虽然这个术语并不像我想象的那么广泛.据我说:
Haskell中的减少(或评估)仅发生在严格点处.
所以,问题是:什么,准确地说,是Haskell的严格点?我的直觉说main
,seq
/爆炸模式,模式匹配以及IO
通过执行的任何动作main
都是主要的严格点,但我不知道为什么我知道这一点.
(另外,如果他们不叫"严点",什么是他们叫什么名字?)
我想一个好的答案将包括一些关于WHNF等的讨论.我也想象它可能触及lambda演算.
编辑:关于这个问题的其他想法.
正如我在这个问题上的反思,我认为在严格点的定义中添加一些东西会更清楚.严格点可以具有不同的上下文和不同的深度(或严格性).回到我的定义"Haskell的减少只发生在严格点",让我们在这个定义中添加这个子句:"只有在评估或减少周围环境时才会触发严格点."
所以,让我试着让你开始我想要的那种答案.main
是严格的一点.它被特别指定为其上下文的主要严格点:程序.当main
评估程序(的上下文)时,激活main的严格点.主要深度是最大的:必须进行全面评估.Main通常由IO动作组成,它们也是严格点,其背景是main
.
现在你尝试:seq
用这些术语讨论和模式匹配.解释功能应用的细微差别:它是如何严格的?怎么回事?怎么样deepseq
?let
和case
陈述?unsafePerformIO
?Debug.Trace
?顶级定义?严格的数据类型?邦模式?等等.这些项目中有多少只能用seq或模式匹配来描述?
这可能现在有点模糊,但我一直在想这一段时间.据我所知!
,可以确保在构造值之前评估数据构造函数的参数:
data Foo = Bar !Int !Float
Run Code Online (Sandbox Code Playgroud)
我经常认为懒惰是一件好事.现在,当我浏览消息来源时,我会看到比非!
变体更严格的字段.
这有什么好处,为什么我不应该把它保持懒惰呢?
未装箱的类型,比如Int#
,和严格的功能,f (!x) = ...
是不同的,但我看到概念上的相似性 - 他们在某种程度上不允许暴力/懒惰.如果Haskell是像Ocaml这样的严格语言,那么每个函数都是严格的,并且每个类型都是未装箱的.unboxed类型与强制执行之间的关系是什么?
在Haskell中,经常提到与懒惰评估相关的术语脊柱严格性.虽然我对这意味着有一个模糊的理解,但对于以下方面有一个更具体的解释会更好:
我有一段代码,使用的概率分布重复采样sequence
.在道德上,它做了这样的事情:
sampleMean :: MonadRandom m => Int -> m Float -> m Float
sampleMean n dist = do
xs <- sequence (replicate n dist)
return (sum xs)
Run Code Online (Sandbox Code Playgroud)
除了它有点复杂.实际的代码我感兴趣的是函数likelihoodWeighting
在此Github上回购.
我注意到运行时间非线性地缩放n
.特别是,一旦n
超过某个值,它就会达到内存限制,并且运行时间会爆炸.我不确定,但我认为这是因为sequence
正在构建一长串的thunk,直到调用才会得到评估sum
.
一旦我通过大约100,000个样本,该程序就会慢慢爬行.我想优化它(我的感觉是1000万个样本应该不是问题)所以我决定对它进行分析 - 但是我在理解分析器的输出方面遇到了一些麻烦.
剖析
我在一个main.hs
运行我的函数的文件中创建了一个简短的可执行文件 这是做的输出
$ ghc -O2 -rtsopts main.hs
$ ./main +RTS -s
Run Code Online (Sandbox Code Playgroud)
我注意到的第一件事 - 它分配了近1.5 GB的堆,并将60%的时间花在垃圾收集上.这通常表明懒惰太多了吗?
1,377,538,232 bytes allocated in the heap
1,195,050,032 bytes copied during GC
169,411,368 bytes maximum residency (12 …
Run Code Online (Sandbox Code Playgroud) Haskell有两个左侧折叠函数用于列表:foldl
和"严格"版本foldl'
.非严格的问题foldl
是它构建了一个thunk的塔:
foldl (+) 0 [1..5]
--> ((((0 + 1) + 2) + 3) + 4) + 5
--> 15
Run Code Online (Sandbox Code Playgroud)
这会浪费内存,如果列表中的项目太多,可能会导致堆栈溢出. foldl'
另一方面,强制累加器在每个项目上.
但是,就我所知,foldl'
在语义上等同于foldl
.评估foldl (+) 0 [1..5]
头部正常形式需要在某个时刻强制累加器.如果我们不需要头部正常形式,我们就不会foldl (+) 0 [1..5]
开始评估.
有没有令人信服的理由,人们会想要foldl
超过那个的行为foldl'
?
例如,我有一个fnB :: a -> Bool
在fnA :: Bool
返回之前没有意义的操作False
.CI中可以将这两个操作组合在一个if
块中:
if( fnA && fnB(a) ){ doSomething; }
Run Code Online (Sandbox Code Playgroud)
和C将保证fnB
在fnA
返回false 之前不会执行.
但Haskell是惰性的,并且,通常也不能保证什么操作将首先执行,直到我们不使用seq
,$!
或别的东西,使我们的代码严格.一般来说,这就是我们需要快乐的事情.但是使用&&
运算符,我希望fnB
在fnA
返回结果之前不会对其进行求值.Haskell提供这样的保证&&
吗?fnB
即使fnA
返回False,Haskell也会评估吗?
下列
(&&) :: Bool -> Bool -> Bool
False && _ = False
True && False = False
True && True = True
Run Code Online (Sandbox Code Playgroud)
具有所需的短路特性False && undefined ? False
.第一个子句在右边的参数中是非严格的,保证在尝试任何其他操作之前进行检查.
显然,如果我改变顺序甚至不发布功能,它仍然有效
both :: (Bool,Bool) -> Bool
both (True,False) = False
both (True, True) = True
both (False, _) = False
Prelude> both (False, undefined)
False
Run Code Online (Sandbox Code Playgroud)
但这实际上是由标准保证的吗?与条款的顺序不同,模式的评估顺序在这里并不十分清楚.在确定snd元素之前,我是否可以确定匹配(True,False)
将在(False,_)
确定后立即中止?
我看过很多会谈/阅读博客文章,你应该有严格的字段,data
以避免各种性能问题,例如:
data Person = Person
{ personName :: !Text
, personBirthday :: !UTCTime
}
Run Code Online (Sandbox Code Playgroud)
这对我来说很有意义.由于对该数据的函数操作是惰性的,因此不会牺牲可组合性.
但是,如果我添加一个Maybe
字段:
data Person = Person
{ personName :: !Text
, personBirthday :: !UTCTime
, personAddress :: !(Maybe Address)
}
Run Code Online (Sandbox Code Playgroud)
我将懒惰引入数据结构,毕竟Maybe
是一个控制结构.是不是可以毫无价值地躲在Just
构造函数后面?
但是,有严格Maybe
的strict
或通过strict-base-types
.但是根据反向依赖(strict,strict-base-types),它们并没有被广泛使用.
所以问题是:为什么Maybe
在非控制数据定义中应该或不应该使用strict ?