弱头范式(WHNF)是什么意思? 头部正常形式(HNF)和正常形式(NF)是什么意思?
熟悉的seq函数将表达式计算为我们称之为head的表达式 正常形式(缩写为HNF)。一旦到达最外层,它就会停止 构造函数(“头”)。这与正常形式(NF)不同 表达式被完全评估。
你也会听到Haskell程序员提到弱头正常形式 (WHNF)。对于正常数据,弱头正常形式与头部相同 正常形式。差异只出现在功能上,也是如此 深深地关注我们。
我已经阅读了一些资源和定义(Haskell Wiki和Haskell Mail List以及Free Dictionary),但我不明白。有人可以举例或提供外行定义吗?
我猜它会类似于:
WHNF = thunk : thunk
HNF = 0 : thunk
NF = 0 : 1 : 2 : 3 : []
seq
和($!)
如何与WHNF和HNF相关?
我仍然感到困惑。我知道有些答案会忽略HNF。通过阅读各种定义,似乎WHNF和HNF中的常规数据之间没有区别。但是,它似乎与功能有所区别。如果没有差异,为什么seq
需要foldl'
?
另一个混淆点来自Haskell Wiki,它指出seq
缩减为WHNF,并且对以下示例不做任何操作。然后他们说他们必须使用seq
来强制进行评估。这不是强迫它到HNF吗?
常见的新手堆栈溢出代码:
myAverage = uncurry (/) . foldl' (\(acc, len) x -> (acc+x, len+1)) (0,0)
了解seq和弱头正常形式(whnf)的人可以 马上明白这里出了什么问题。 (acc + x,len + 1)已经存在 在whnf,所以seq,它将值减少到whnf,对此没有任何作用。 这段代码将像原始的foldl示例一样构建thunks, 他们只是在一个元组里面。解决方案只是强迫 元组的组成部分,例如
myAverage = uncurry (/) . foldl' (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)
答案 0 :(得分:381)
我会尝试用简单的术语来解释。正如其他人所指出的那样,头部正常形式不适用于Haskell,所以我不会在这里考虑它。
完全评估正常形式的表达式,并且不能再进一步评估子表达式(即它不包含未评估的thunk)。
这些表达式都是正常形式:
42
(2, "hello")
\x -> (x + 1)
这些表达式不是正常形式:
1 + 2 -- we could evaluate this to 3
(\x -> x + 1) 2 -- we could apply the function
"he" ++ "llo" -- we could apply the (++)
(1 + 1, 2 + 2) -- we could evaluate 1 + 1 and 2 + 2
弱头正常形式的表达式已经被评估到最外层的数据构造函数或lambda抽象( head )。子表达式可能已经或未经过评估。因此,每个正常形式的表达式也都是弱头正常形式,尽管相反的情况并不成立。
要确定表达式是否处于弱头正常形式,我们只需要查看表达式的最外层部分。如果它是数据构造函数或lambda,则它处于弱头正常形式。如果它是一个功能应用程序,那就不是。
这些表达式处于弱头正常状态:
(1 + 1, 2 + 2) -- the outermost part is the data constructor (,)
\x -> 2 + 2 -- the outermost part is a lambda abstraction
'h' : ("e" ++ "llo") -- the outermost part is the data constructor (:)
如上所述,上面列出的所有正常形式表达式也都是弱头正常形式。
这些表达式不是弱头正常形式:
1 + 2 -- the outermost part here is an application of (+)
(\x -> x + 1) 2 -- the outermost part is an application of (\x -> x + 1)
"he" ++ "llo" -- the outermost part is an application of (++)
将表达式评估为弱头正常形式可能需要首先将其他表达式计算为WHNF。例如,要评估1 + (2 + 3)
到WHNF,我们首先要评估2 + 3
。如果评估单个表达式导致这些嵌套评估过多,则结果是堆栈溢出。
当您构建一个不会生成任何数据构造函数或lambdas的大型表达式,直到对其进行大部分计算时,就会发生这种情况。这些通常是由foldl
:
foldl (+) 0 [1, 2, 3, 4, 5, 6]
= foldl (+) (0 + 1) [2, 3, 4, 5, 6]
= foldl (+) ((0 + 1) + 2) [3, 4, 5, 6]
= foldl (+) (((0 + 1) + 2) + 3) [4, 5, 6]
= foldl (+) ((((0 + 1) + 2) + 3) + 4) [5, 6]
= foldl (+) (((((0 + 1) + 2) + 3) + 4) + 5) [6]
= foldl (+) ((((((0 + 1) + 2) + 3) + 4) + 5) + 6) []
= (((((0 + 1) + 2) + 3) + 4) + 5) + 6
= ((((1 + 2) + 3) + 4) + 5) + 6
= (((3 + 3) + 4) + 5) + 6
= ((6 + 4) + 5) + 6
= (10 + 5) + 6
= 15 + 6
= 21
请注意,在将表达式转换为弱头正常形式之前,它必须走得很深。
您可能想知道,为什么Haskell不会提前减少内部表达式?那是因为Haskell的懒惰。由于不能一般地假设需要每个子表达式,因此表达式将从外部进行评估。
(GHC有一个严格的分析器,可以检测一些总是需要子表达式的情况,然后它可以提前对它进行评估。但这只是一个优化,你不应该依赖它来避免溢出)。
另一方面,这种表达是完全安全的:
data List a = Cons a (List a) | Nil
foldr Cons Nil [1, 2, 3, 4, 5, 6]
= Cons 1 (foldr Cons Nil [2, 3, 4, 5, 6]) -- Cons is a constructor, stop.
为了避免在我们知道必须评估所有子表达式时构建这些大表达式,我们希望强制提前评估内部部分。
seq
seq
是一个特殊函数,用于强制计算表达式。它的语义是seq x y
意味着每当y
被评估为弱头正常形式时,x
也被评估为弱头正常形式。
它是foldl'
定义中使用的其他地方,foldl
的严格变体。
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
foldl'
的每次迭代都会强制累加器到WHNF。因此,它避免了构建大型表达式,因此避免了堆栈溢出。
foldl' (+) 0 [1, 2, 3, 4, 5, 6]
= foldl' (+) 1 [2, 3, 4, 5, 6]
= foldl' (+) 3 [3, 4, 5, 6]
= foldl' (+) 6 [4, 5, 6]
= foldl' (+) 10 [5, 6]
= foldl' (+) 15 [6]
= foldl' (+) 21 []
= 21 -- 21 is a data constructor, stop.
但是正如HaskellWiki上的例子所提到的那样,这并不能在所有情况下保存你,因为累加器只被评估为WHNF。在示例中,累加器是一个元组,因此它只会强制评估元组构造函数,而不是acc
或len
。
f (acc, len) x = (acc + x, len + 1)
foldl' f (0, 0) [1, 2, 3]
= foldl' f (0 + 1, 0 + 1) [2, 3]
= foldl' f ((0 + 1) + 2, (0 + 1) + 1) [3]
= foldl' f (((0 + 1) + 2) + 3, ((0 + 1) + 1) + 1) []
= (((0 + 1) + 2) + 3, ((0 + 1) + 1) + 1) -- tuple constructor, stop.
为了避免这种情况,我们必须这样做,以便评估元组构造函数强制评估acc
和len
。我们使用seq
。
f' (acc, len) x = let acc' = acc + x
len' = len + 1
in acc' `seq` len' `seq` (acc', len')
foldl' f' (0, 0) [1, 2, 3]
= foldl' f' (1, 1) [2, 3]
= foldl' f' (3, 2) [3]
= foldl' f' (6, 3) []
= (6, 3) -- tuple constructor, stop.
答案 1 :(得分:41)
Haskell Wikibooks Thunks and Weak Head Normal Form中关于description of laziness的部分提供了对WHNF的非常好的描述以及这个有用的描述:
逐步评估值(4,[1,2])。第一阶段是 完全没有评价;所有后续表格都在WHNF中,最后一个 一个也是正常形式。
答案 2 :(得分:26)
Haskell程序是表达式,它们通过执行评估来运行。
要评估表达式,请按其定义替换所有函数应用程序。你这样做的顺序并不重要,但它仍然很重要:从最外面的应用程序开始,从左到右进行;这称为懒惰评估。
示例:
take 1 (1:2:3:[])
=> { apply take }
1 : take (1-1) (2:3:[])
=> { apply (-) }
1 : take 0 (2:3:[])
=> { apply take }
1 : []
当没有更多功能应用程序需要替换时,评估停止。结果是正常形式(或缩小正常形式,RNF)。无论您评估表达式的顺序如何,您总是会以相同的正常形式结束(但仅在评估终止时)。
懒惰评估的描述略有不同。也就是说,它表示你应该只评估所有内容弱头正常形式。在WHNF中,表达式恰好有三种情况:
constructor expression_1 expression_2 ...
(+) 2
或sqrt
\x -> expression
换句话说,表达式的头部(即最外面的函数应用程序)无法进一步求值,但函数参数可能包含未评估的表达式。
WHNF的例子:
3 : take 2 [2,3,4] -- outermost function is a constructor (:)
(3+1) : [4..] -- ditto
\x -> 4+5 -- lambda expression
注释
答案 3 :(得分:25)
在http://foldoc.org/Weak+Head+Normal+Form处给出了一个很好的解释例子。头部正规形式甚至简化了函数抽象中表达式的位,而“弱”头部正规形式在函数抽象中停止。
从源头上,如果你有:
\ x -> ((\ y -> y+x) 2)
是弱头正常形式,但不是头部正常形式...因为可能的应用程序被卡在一个无法评估的函数内。
实际头部正常形式难以有效实施。它需要在函数内部进行调整。所以弱头标准形式的优点是你仍然可以将函数实现为opaque类型,因此它与编译语言和优化更兼容。
答案 4 :(得分:12)
WHNF不希望评估lambda的主体,所以
WHNF = \a -> thunk
HNF = \a -> a + c
seq
希望它的第一个参数在WHNF中,所以
let a = \b c d e -> (\f -> b + c + d + e + f) b
b = a 2
in seq b (b 5)
评估为
\d e -> (\f -> 2 + 5 + d + e + f) 2
而不是,什么将使用HNF
\d e -> 2 + 5 + d + e + 2
答案 5 :(得分:5)
基本上,假设你有某种thunk,t
。
现在,如果我们要评估t
到WHNF或NHF,除了函数之外它们是相同的,我们会发现我们得到类似的东西
t1 : t2
其中t1
和t2
是thunk。在这种情况下,t1
将是您的0
(或更确切地说,是0
的thunk,没有额外的拆箱)
seq
和$!
评估WHNF。注意
f $! x = seq x (f x)
答案 6 :(得分:1)
我意识到这是一个老问题,但这里是 WHNF、HNF 和 NF 的明确数学定义。在纯 lambda 演算中:
一个术语在 NF 中,如果它是这样的形式
λ x1. λ x2. ... λ xn. (x t1 t2 ... tm)
其中 x
是一个变量,而 t1, t2, ..., tm
在 NF 中。
一个术语在 HNF 中,如果它是这样的形式
λ x1. λ x2. ... λ xn. (x e1 e2 ... em)
其中 x
是变量,e1, e2, ..., em
是任意项。
一个术语在 WHNF 中,如果它是任何术语 λ x. e
的 lambda 术语 e
或者它的形式
x e1 e2 ... em
其中 x
是变量,e1, e2, ..., em
是任意项。
现在考虑具有 构造函数 a,b,c...
na, nb, nc...
的编程语言,这意味着只要 t1, t2, ..., tm
处于 NF,那么术语 a t1 t2 ... tm
其中 m = na
是一个 redex 并且可以被评估。例如,Haskell 中的加法构造函数 +
具有元数 2
,因为它仅在以标准形式给出两个参数时评估(在这种情况下是整数,它们本身可以被视为空构造函数)。
一个术语在 NF 中,如果它是这样的形式
λ x1. λ x2. ... λ xn. (x t1 t2 ... tm)
其中 x
是变量或具有 n
的参数 m < n
的构造函数,并且 t1, t2, ..., tm
在 NF 中。
一个术语在 HNF 中,如果它是这样的形式
λ x1. λ x2. ... λ xn. (x e1 e2 ... em)
其中 x
是变量 n
的变量或构造函数,而 e1, e2, ... em
是任意项只要第一个 n
参数是 不是全部在NF中。
一个术语在 WHNF 中,如果它是任何术语 λ x. e
的 lambda 术语 e
或者它的形式
x e1 e2 ... em
其中 x
是变量 n
的变量或构造函数,而 e1, e2, ... em
是任意项只要第一个 n
参数是 不是全部在NF中。
特别是,NF 中的任何术语都在 HNF 中,HNF 中的任何术语都在 WHNF 中,但反之则不然。
答案 7 :(得分:0)
在图归约的实现中,对HNF的惰性评估会迫使您处理lambda演算的名称捕获问题,而对WHNF的惰性评估则可以避免这种情况。
Simon Peyton Jones在Chapter 11 of The Implementation of Functional Programming Languages中对此进行了解释。