弱头范式 (WHNF) 是什么意思? 头部范式 (HNF) 和 范式 (NF) 是什么意思?
Real World Haskell 状态:
我已经阅读了一些资源和定义( 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 吗?- Haskell Wiki on Stackoverflow
最佳答案
我将尝试用简单的术语进行解释。正如其他人指出的那样,头部范式不适用于 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 抽象(头部)。子表达式可能会或可能不会被评估。因此,每个范式表达式也是弱头范式,尽管相反的情况并不普遍。
要确定一个表达式是否是弱头范式,我们只需要查看表达式的最外层部分。如果它是数据构造函数或 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
。如果对单个表达式求值导致这些嵌套求值过多,则结果是堆栈溢出。当您构建一个大型表达式时会发生这种情况,该表达式在其大部分已被评估之前不会产生任何数据构造函数或 lambda。这些通常是由
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.
关于haskell - 什么是弱头范式?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/6872898/