什么是弱头范式?


290

什么弱头范式(WHNF)是什么意思?什么头标准型(HNF)和范式(NF)是什么意思?

现实世界中的Haskell指出:

熟悉的seq函数将表达式计算为我们称为head正常形式(缩写为HNF)的表达式。一旦到达最外部的构造函数(“头”),它将停止。这与完全评估表达式的正常形式(NF)不同。

您还将听到Haskell程序员提到弱头正常形式(WHNF)。对于正常数据,弱磁头正常形式与磁头正常形式相同。差异仅在功能上出现,在这里太过深刻了,我们不必担心。

我已经阅读了一些资源和定义(Haskell WikiHaskell邮件列表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中,因此将swnf值减小的seq对此没有任何作用。就像原始的foldl示例一样,此代码将构建thunk,它们将位于元组中。解决方案只是强制使用元组的组件,例如

myAverage = uncurry (/) . foldl' 
          (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)

- 哈斯克尔维基#2


1
通常,我们谈到WHNF和RNF。(RNF是您所说的NF)
另一种方式是

5
@monadic RNF中的R代表什么?
dave4420 2011年

7
@ dave4420:减少
马克

Answers:


399

我将尝试用简单的术语进行解释。正如其他人指出的那样,头部范式不适用于Haskell,因此在这里我将不考虑它。

正常形式

正常形式的表达式已被完全求值,子表达式无法再求值(即,它不包含未求值的重排)。

这些表达式均为正常形式:

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。如果对单个表达式求值导致这些嵌套求值过多,则结果是堆栈溢出。

当您构建一个大型表达式,只有在大部分表达式被求值后,它才会生成任何数据构造函数或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进行求值,因此这并不能在所有情况下为您省钱。在示例中,累加器是一个元组,因此它将仅强制评估元组构造函数,而不是accor 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.

31
头部正常形式要求也减小lambda的身体,而弱头部正常形式不具有此要求。所以\x -> 1 + 1是WHNF但不HNF。
hammar 2011年

维基百科指出,HNF是“ [a]术语为头部正常形式,如果头部没有beta-redex”。Haskell是“弱”的,因为它没有beta-redex子表达式吗?

严格的数据构造函数如何发挥作用?他们就像在呼吁seq自己的论点吗?
Bergi 2014年

1
@CaptainObvious:1 + 2既不是NF也不是WHNF。表达式并非总是以正常形式出现。
hammar

2
@Zorobay:为了打印结果,GHCI最终完全根据NF评估表达式,而不仅仅是对WHNF进行评估。区分这两个变量之间差异的一种方法是使用启用内存统计信息:set +s。然后,您会看到foldl' f分配的thunk最终超过foldl' f'
hammar

43

Haskell Wikibooks上关于懒惰的描述中的“ Thunks 和弱头范式 ”部分对WHNF进行了很好的描述,并提供了以下有用的描述:

逐步评估值(4,[1,2])。 第一阶段是完全未被评估的; 所有后续形式均为WHNF,最后一种形式也为正常形式。

逐步评估值(4,[1,2])。第一阶段是完全未被评估的;所有后续形式均为WHNF,最后一种形式也为正常形式。


5
我知道人们说要忽略头部范式,但是您可以在该图中给出一个例子,您知道头部范式是什么样子吗?
CMCDragonkai

28

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
  • Lambda表达式: \x -> expression

换句话说,表达式的头部(即最外面的函数应用程序)无法再进行求值,但是函数参数可能包含未求值的表达式。

WHNF的示例:

3 : take 2 [2,3,4]   -- outermost function is a constructor (:)
(3+1) : [4..]        -- ditto
\x -> 4+5            -- lambda expression

笔记

  1. WHNF中的“头”不引用列表的头,而是引用最外层的功能应用程序。
  2. 有时,人们称未经评估的表达式为“笨拙”,但我认为这不是理解它的好方法。
  3. 头正常形式(HNF)与Haskell无关。它与WHNF的不同之处在于,lambda表达主体也得到了一定程度的评估。

是利用seqfoldl'力评价从WHNF到HNF?

1
@snmcdonald:不,Haskell不使用HNF。评估seq expr1 expr2expr1先评估WHNF 的第一个表达式,然后再评估第二个表达式expr2
Heinrich Apfelmus 2011年

26

有关示例的良好解释,请参见http://foldoc.org/Weak+Head+Normal+Form。 正常形式甚至简化了函数抽象内的表达式位,而“ weak” head正常形式则在函数抽象处停止。

从源头来看,如果您有:

\ x -> ((\ y -> y+x) 2)

那是弱的普通形式,而不是普通形式...因为可能的应用程序卡在了尚无法评估的函数中。

实际的头部正常形式将难以有效实施。这将需要在函数内部四处查找。因此,弱头范式的优点是您仍然可以将函数实现为不透明类型,因此它与编译语言和优化程序更加兼容。


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

或我误解了该示例,或者在WHNF和HNF中混合了1和2。

5

基本上,假设您有某种重击, t

现在,如果我们要评估 t WHNF或NHF,除了功能相同,我们会发现类似

t1 : t2哪里t1t2是暴徒。在这种情况下,t1将是您0(或更确切地说是0没有任何额外的拆箱操作)

seq$!评估WHNF。注意

f $! x = seq x (f x)

1
@snmcdonald忽略HNF。seq表示,将其评估为WHNF时,请评估WHNF的第一个参数。
可替代
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.