关于定义:Haskell:什么是弱头正常形式?

Haskell: What is Weak Head Normal Form?

弱头正常形态(whnf)是什么意思?头部正常形态(hnf)和正常形态(nf)是什么意思?

现实世界哈斯克尔说:

The familiar seq function evaluates an expression to what we call head
normal form (abbreviated HNF). It stops once it reaches the outermost
constructor (the"head"). This is distinct from normal form (NF), in
which an expression is completely evaluated.

You will also hear Haskell programmers refer to weak head normal form
(WHNF). For normal data, weak head normal form is the same as head
normal form. The difference only arises for functions, and is too
abstruse to concern us here.

我已经阅读了一些资源和定义(haskell wiki和haskell邮件列表和免费词典),但我不明白。有人能举个例子或者提供一个外行定义吗?

我想这应该类似于:

1
2
3
4
5
WHNF = thunk : thunk

HNF = 0 : thunk

NF = 0 : 1 : 2 : 3 : []

seq($!)与whnf和hnf有什么关系?

更新

我还是很困惑。我知道有些答案说忽略了HNF。从阅读各种定义来看,WHNF和HNF中的常规数据似乎没有区别。然而,在函数方面似乎确实存在差异。如果没有区别,为什么foldl'需要seq

另一个困惑点来自haskell wiki,它指出seq减少到whnf,并且不会对下面的示例做任何操作。然后他们说他们必须使用seq来强制评估。这不是强迫它去HNF吗?

Common newbie stack overflowing code:

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

People who understand seq and weak head normal form (whnf) can
immediately understand what goes wrong here. (acc+x, len+1) is already
in whnf, so seq, which reduces a value to whnf, does nothing to this.
This code will build up thunks just like the original foldl example,
they'll just be inside a tuple. The solution is just to force the
components of the tuple, e.g.

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

-StackOverflow上的haskell wiki


我尽量用简单的术语解释一下。正如其他人所指出的,头部正态形式不适用于haskell,所以在这里我不会考虑它。

正常形式

正常形式的表达式是完全计算的,不能再进一步计算子表达式(即它不包含未计算的thunk)。

这些表达式都是正常形式:

1
2
3
42
(2,"hello")
\x -> (x + 1)

这些表达式不是正常形式:

1
2
3
4
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
2
3
(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
3
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被计算之前不会生成任何数据构造函数或lambda。这通常是由于使用foldl而引起的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

请注意,在将表达式转换为弱头正常形式之前,它必须非常深入。

你可能会想,为什么哈斯克尔不提前减少内在表达?这是因为哈斯克尔的懒惰。由于一般不能假定每个子表达式都是必需的,因此表达式是从外到内计算的。

(GHC有一个严格的分析程序,它可以检测出某些情况下总是需要子表达式,然后可以提前对其进行评估。然而,这只是一个优化,您不应该依赖它来避免溢出)。

另一方面,这种表达是完全安全的:

1
2
3
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.

为了避免在我们知道所有子表达式都必须计算时构建这些大型表达式,我们希望强制提前计算内部部分。

埃多克斯1〔3〕

seq是一个特殊函数,用于强制计算表达式。它的语义是:seq x y表示,当y被评价为弱头正态形式时,x也被评价为弱头正态形式。

它是foldl'定义中使用的其他地方之一,是foldl的严格变体。

1
2
foldl' f a []     = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs

每次迭代foldl'都会迫使蓄能器进入WHNF。因此,它避免了构建大型表达式,从而避免了堆栈溢出。

1
2
3
4
5
6
7
8
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。在本例中,累加器是一个元组,因此它只强制对元组构造函数进行评估,而不是强制acclen

1
2
3
4
5
6
7
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.

为了避免这种情况,我们必须这样做,以便对元组构造函数进行评估,从而强制对acclen进行评估。我们通过使用seq来实现这一点。

1
2
3
4
5
6
7
8
9
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 wikibooks中有关thunks和弱头正常形式的章节"懒惰的描述"提供了对whnf的非常好的描述以及这一有用的描述:

Evaluating the value (4, [1, 2]) step by step. The first stage is completely unevaluated; all subsequent forms are in WHNF, and the last one is also in normal form.

Evaluating the value (4, [1, 2]) step by step. The first stage is
completely unevaluated; all subsequent forms are in WHNF, and the last
one is also in normal form.


haskell程序是表达式,通过执行评估来运行。

要计算表达式,请将所有函数应用程序替换为其定义。这样做的顺序并不重要,但仍然很重要:从最外层的应用程序开始,从左到右进行;这称为惰性评估。

例子:

1
2
3
4
5
6
7
   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 ...
  • 参数太少的内置函数,如(+) 2sqrt
  • lambda表达式:\x -> expression

换句话说,表达式的头(即最外面的函数应用程序)无法进一步计算,但函数参数可能包含未计算的表达式。

WHNF示例:

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

笔记

  • whnf中的"head"不是指列表的head,而是指最外层的函数应用程序。
  • 有时,人们会把未估值的表达称为"thunks",但我不认为这是理解它的好方法。
  • 头部正常形态(hnf)与haskell无关。它与whnf的不同之处在于lambda表达式的主体也在某种程度上进行了计算。

  • 在http://foldoc.org/weak+head+normal+form head-normal-form中给出了一个很好的示例解释,它甚至简化了函数抽象中表达式的位,而"weak"head-normal-form则停止函数抽象。

    如果你有:

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

    这是弱头正常形态,但不是头正常形态…因为可能的应用程序被卡在一个还无法评估的函数中。

    实际的头部正常形态将难以有效实施。它需要在函数内部进行搜索。因此,弱头标准形式的优点是,您仍然可以将函数作为不透明类型来实现,因此它更兼容编译语言和优化。


    whnf不希望对lambda体进行评估,因此

    1
    2
    WHNF = \a -> thunk
    HNF = \a -> a + c

    seq希望它的第一个论点在whnf中,所以

    1
    2
    3
    let a = \b c d e -> (\f -> b + c + d + e + f) b
        b = a 2
    in seq b (b 5)

    计算结果为

    1
    \d e -> (\f -> 2 + 5 + d + e + f) 2

    而不是,什么会使用hnf

    1
    \d e -> 2 + 5 + d + e + 2


    基本上,假设您有某种类型的thunk,t

    现在,如果我们想将t计算为whnf或nhf,除了函数外,它们是相同的,我们会发现

    t1 : t2,其中t1t2是雷声。在这种情况下,t1将是你的0(或者更确切地说,一个雷鸣般的0没有额外拆箱)

    seq$!评估了whnf。注意

    1
    f $! x = seq x (f x)