繁体   English   中英

什么是弱头范式?

[英]What is Weak Head Normal Form?

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

真实世界 Haskell指出:

熟悉的 seq 函数将表达式计算为我们所说的头部范式(缩写为 HNF)。 一旦到达最外面的构造函数(“头部”),它就会停止。 这与标准形式 (NF) 不同,在标准形式 (NF) 中,完全评估表达式。

您还将听到 Haskell 程序员提到弱头范式 (WHNF)。 对于普通数据,弱头范式与头范式相同。 区别只出现在函数上,太深奥了,我们这里不关心。

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

我猜它会类似于:

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 中,因此将值减少到 whnf 的 seq 对此没有任何作用。 这段代码将像原始 foldl 示例一样构建 thunk,它们只是在一个元组中。 解决方案只是强制元组的组件,例如

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

- Stackoverflow 上的 Haskell Wiki

我将尝试用简单的术语进行解释。 正如其他人指出的那样,头部范式不适用于 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 如果对单个表达式求值导致这些嵌套求值过多,则结果是堆栈溢出。

当您构建一个大型表达式时会发生这种情况,该表达式在评估大部分内容之前不会生成任何数据构造函数或 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

笔记

  1. WHNF 中的“头”不是指列表的头,而是指最外层的函数应用程序。
  2. 有时,人们称未评估的表达式为“thunk”,但我认为这不是理解它的好方法。
  3. 头部范式(HNF) 与 Haskell 无关。 它与 WHNF 的不同之处在于 lambda 表达式的主体也在某种程度上进行了评估。

http://foldoc.org/Weak+Head+Normal+Form给出了一个很好的例子解释 头部范式甚至简化了函数抽象内部的表达式的位,而“弱”头部范式在函数抽象处停止.

从源头来看,如果您有:

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

这是弱头部范式,但不是头部范式......因为可能的应用程序被困在一个无法评估的函数内。

实际的头部范式将难以有效实施。 它需要在函数内部四处探索。 所以弱头范式的优点是你仍然可以将函数实现为不透明类型,因此它更兼容编译语言和优化。

WHNF 不想评估 lambdas 的主体,因此

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

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

现在,如果我们想将t评估为 WHNF 或 NHF,除了函数之外,它们是相同的,我们会发现我们得到类似

t1 : t2其中t1t2是 thunk。 在这种情况下, t1将是您的0 (或者更确切地说,如果没有额外的拆箱,则为0

seq$! 评估 WHNF。 注意

f $! x = seq x (f x)

在图缩减的实现中,对 HNF 的惰性求值迫使您处理 lambda 演算的名称捕获问题,而对 WHNF 的惰性求值可以让您避免它。

这在 Simon Peyton Jones 的函数式编程语言的实现的第 11 章中进行了解释。

我意识到这是一个老问题,但这里是 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是任意项。

  • 如果一个项是 lambda 项λ x. e ,则该项在 WHNF 中λ x. e λ x. e对于任何术语e或者如果它是这样的形式

    x e1 e2 ... em

    其中x是一个变量, e1, e2, ..., em是任意项。


现在考虑一种具有na, nb, nc...构造函数a,b,c...编程语言,这意味着只要t1, t2, ..., tm在 NF 中,那么术语a t1 t2 ... tm其中m = na是一个 redex 并且可以被评估。 例如,Haskell 中的加法构造函数+具有 arity 2 ,因为它仅在以标准形式给出两个参数时才进行计算(在这种情况下,整数,它们本身可以被视为空构造函数)。

  • 一个项在 NF 中,如果它是这样的形式

    λ x1. λ x2. ... λ xn. (x t1 t2 ... tm)

    其中x是变量或具有m < n的元数n的构造函数,并且t1, t2, ..., tm在 NF 中。

  • 一个术语在 HNF 中,如果它是这样的形式

    λ x1. λ x2. ... λ xn. (x e1 e2 ... em)

    其中xn的变量或构造函数,并且e1, e2, ... em是任意项,只要前n参数不是全部在 NF 中。

  • 如果一个项是 lambda 项λ x. e ,则该项在 WHNF 中λ x. e λ x. e对于任何术语e或者如果它是这样的形式

    x e1 e2 ... em

    其中xn的变量或构造函数,并且e1, e2, ... em是任意项,只要前n参数不是全部在 NF 中。


特别地,NF 中的任何项都在 HNF 中,HNF 中的任何项都在 WHNF 中,但反之则不然。

头部范式意味着没有头部还原

(λx.((λy.y+x)b))b

减少到:R,对于 redex(是的,里面还有另一个 redex,但这无关紧要)。 这是一个 head redex,因为它是最左边的 redex(唯一的 redex),并且在它之前没有 lambda 项(变量或 lambda 表达式(应用程序或抽象)),只有 0 到 n 个抽象器(如果 R 是一个 redex (λx.A)B R 的抽象器是λx ),在这种情况下是 0。

因为有一个头部 redex,它不在 HNF 中,因此它也不在 NF 中,因为有一个 redex。

WHNF 意味着它是一个 lambda 抽象或在 HNF 中。 以上不在 HNF 中,也不是 lambda 抽象,而是一个应用程序,因此不在 WHNF 中。

λx.((λy.y+x)b)b在 WHNF

它是一个 lambda 抽象,但不是在 HNF 中,因为有一个头部λx.Rb

减少到λx.((b+x)b) 没有 redex,因此它是正常形式。

考虑λx.((λy.zyx)b) ,它简化为λx.R ,所以它不在 HNF 中。 λx.(k(λy.zyx)b)简化为λx.kR因此它在 HNF 但不是 NF。

所有 NF 都在 HNF 和 WHNF 中。 所有 HNF 都是 WHNF。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM