弱头范式 (WHNF) 是什么意思? 头 范式 ( HNF) 和 范式 (NF) 是什么意思?
真实世界的 Haskell状态:
seq熟悉的函数将表达式计算为我们所说的 头部范式 (缩写为 HNF)。一旦到达最外层的构造函数(“头部”),它就会停止。这与 标准形式 (NF) 不同,后者完全计算表达式。 你还会听到 Haskell 程序员提到 弱 头范式 (WHNF)。对于法线数据,弱头范式与头范式相同。区别只出现在功能上,而且太深奥了,我们在这里不关心。
seq熟悉的函数将表达式计算为我们所说的 头部范式 (缩写为 HNF)。一旦到达最外层的构造函数(“头部”),它就会停止。这与 标准形式 (NF) 不同,后者完全计算表达式。
seq
你还会听到 Haskell 程序员提到 弱 头范式 (WHNF)。对于法线数据,弱头范式与头范式相同。区别只出现在功能上,而且太深奥了,我们在这里不关心。
我已经阅读了一些资源和定义(Haskell Wiki和Haskell Mail List和Free Dictionary),但我不明白。有人可以举个例子或提供一个外行定义吗?
我猜它会类似于:
WHNF = thunk : thunk HNF = 0 : thunk NF = 0 : 1 : 2 : 3 : []
WHNFseq和($!)HNF 有什么关系?
($!)
我仍然很困惑。我知道一些答案说忽略 HNF。从阅读各种定义来看,WHNF 和 HNF 中的常规数据似乎没有区别。但是,在功能方面似乎确实存在差异。如果没有区别,为什么seq需要foldl'?
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 中了,所以将值降为 whnf 的seq(在的定义中foldl')对此没有任何作用. 这段代码将像原始示例一样构建foldlthunk ,它们只是在一个元组中。解决方案只是强制元组的组件,例如 myAverage = uncurry (/) . foldl' (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)
常见新手栈溢出代码:
myAverage = uncurry (/) . foldl' (\(acc, len) x -> (acc+x, len+1)) (0,0)
了解 seq 和弱头范式(whnf)的人可以立即明白这里出了什么问题(acc+x,聽len+1)。已经在 whnf 中了,所以将值降为 whnf 的seq(在的定义中foldl')对此没有任何作用. 这段代码将像原始示例一样构建foldlthunk ,它们只是在一个元组中。解决方案只是强制元组的组件,例如
(acc+x,聽len+1)
foldl
myAverage = uncurry (/) . foldl' (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)
我将尝试用简单的术语来解释。正如其他人所指出的,头部范式不适用于 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. 如果评估单个表达式导致过多的嵌套评估,则结果是堆栈溢出。
1 + (2 + 3)
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 x y意味着每当y被评估为弱头范式时,x也被评估为弱头范式。
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。在下面的示例中,累加器是一个元组,因此它只会强制评估元组构造函数,而不是accor len。
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.
为了避免这种情况,我们必须使评估元组构造函数强制评估accand 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.