Haskell:什么是弱头正常形式?

时间:2011-07-29 12:13:49

标签: haskell definition strictness weak-head-normal-form

弱头范式(WHNF)是什么意思? 头部正常形式(HNF)和正常形式(NF)是什么意思?

Real World Haskell州:

  

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

     

你也会听到Haskell程序员提到弱头正常形式   (WHNF)。对于正常数据,弱头正常形式与头部相同   正常形式。差异只出现在功能上,也是如此   深深地关注我们。

我已经阅读了一些资源和定义(Haskell WikiHaskell 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)

- Haskell Wiki on Stackoverflow

8 个答案:

答案 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。在示例中,累加器是一个元组,因此它只会强制评估元组构造函数,而不是acclen

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

执行此操作
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的非常好的描述以及这个有用的描述:

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.

  

逐步评估值(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 ...
  • 参数太少的内置函数,例如(+) 2sqrt
  • lambda-expression:\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中的“head”不是指列表的头部,而是指最外层的函数应用程序。
  2. 有时,人们会将未评价的表达称为“thunk”,但我认为这不是理解它的好方法。
  3. 头部正常形式(HNF)与Haskell无关。它与WHNF的不同之处在于,lambda表达式的实体也在一定程度上得到了评估。

答案 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其中t1t2是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中对此进行了解释。