哈斯克尔:什么是弱头正常形式?

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

真实世界Haskell说:

熟悉的seq函数将表达式评估为我们称之为头标准形式(简称HNF)的表达式。 它一旦到达最外层的构造函数(“头部”)就会停止。 这不同于正常形式(NF),其中表达被完全评估。

您还将听到Haskell程序员提到弱头标准格式(WHNF)。 对于正常数据,弱磁头标准形式与标准标准形式相同。 功能上的区别只在于,而且在这里关心我们太深奥了。

我已经阅读了一些资源和定义(Haskell Wiki和Haskell邮件列表和自由词典),但我没有得到它。 有人可以举一个例子或提供外行人的定义吗?

我猜测它会类似于:

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)已经在使用,所以seq会将值减小为whnf,对此不做任何事情。 这段代码将像原来的foldl例子一样构建thunk,它们只是在一个元组中。 解决方案只是强制元组的组件,例如

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

-Haskell维基于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 xy意味着每当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.

关于懒惰的Haskell Wikibooks描述中关于Thunks和Weak Head Normal Form的部分提供了WHNF的一个很好的描述以及这个有用的描述:

逐步评估值(4,[1,2])。第一阶段完全没有评估;所有后续形式都在WHNF中,最后一个也是正常形式。

逐步评估值(4,[1,2])。 第一阶段完全没有评估; 所有后续形式都在WHNF中,最后一个也是正常形式。


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表达式: x -> expression
  • 换句话说,表达式的头部(即最外面的函数应用程序)不能被进一步评估,但函数参数可能包含未评估的表达式。

    WHNF的例子:

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

    笔记

  • WHNF中的“头”不是指列表的头部,而是指最外面的函数应用程序。
  • 有时候,人们称未经评估的表达式为“thunk”,但我不认为这是理解它的好方法。
  • 头标准形式 (HNF)与Haskell无关。 它与WHNF的不同之处在于,lambda表达式的主体也在一定程度上被评估。
  • 链接地址: http://www.djcxy.com/p/1143.html

    上一篇: Haskell: What is Weak Head Normal Form?

    下一篇: scale design in Haskell?