什么是弱头范式?
弱头范式 (WHNF) 是什么意思? Head Normal form (HNF) 和 Normal Form (NF) 是什么意思?
现实世界 Haskell 指出:
熟悉的
seq
函数将表达式计算为我们想要的值 称为头范式 (缩写为HNF)。一旦到达就停止 最外面的构造函数(“头”)。这与正常情况不同 形式 (NF),其中表达式被完全求值。您还会听到 Haskell 程序员提到 弱 头部正常 形式(WHNF)。对于普通数据,弱头范式与 头部正常形态。差异仅出现在功能上,而且差异太大 在这里关心我们是很深奥的。
我已经阅读了一些资源和定义(Haskell Wiki 和 Haskell 邮件列表和免费词典),但我不明白。有人可以举个例子或者提供一个外行定义吗?
我猜它类似于:
WHNF = thunk : thunk
HNF = 0 : thunk
NF = 0 : 1 : 2 : 3 : []
How do seq
and ($!)
related to WHNF and HNF?
更新
我还是很困惑。我知道有些答案说忽略 HNF。从阅读各种定义来看,WHNF 和 HNF 中的常规数据似乎没有区别。然而,在功能方面似乎确实存在差异。如果没有区别,为什么 foldl'
需要 seq
?
另一个令人困惑的地方来自 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
(在foldl'
的定义中)将值减少为 whnf,对 这。此代码将像原始的foldl
示例一样构建 thunk,它们只是位于元组内。解决办法就是强制 元组的组成部分,例如myAverage = uncurry (/) 。折叠' (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)
What does Weak Head Normal Form (WHNF) mean? What does Head Normal form (HNF) and Normal Form (NF) mean?
Real World Haskell states:
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.
I have read a few resources and definitions (Haskell Wiki and Haskell Mail List and Free Dictionary) but I don't get it. Can someone perhaps give an example or provide a layman definition?
I am guessing it would be similar to:
WHNF = thunk : thunk
HNF = 0 : thunk
NF = 0 : 1 : 2 : 3 : []
How do seq
and ($!)
relate to WHNF and HNF?
Update
I am still confused. I know some of the answers say to ignore HNF. From reading the various definitions it seems that there is no difference between regular data in WHNF and HNF. However, it does seem like there is a difference when it comes to a function. If there was no difference, why is seq
necessary for foldl'
?
Another point of confusion is from the Haskell Wiki, which states that seq
reduces to WHNF, and will do nothing to the following example. Then they say that they have to use seq
to force the evaluation. Is that not forcing it to HNF?
Common newbie stack overflowing code:
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 theseq
(in the definition offoldl'
), which reduces a value to whnf, does nothing to this. This code will build up thunks just like the originalfoldl
example, they'll just be inside a tuple. The solution is just to force the
components of the tuple, e.g.myAverage = uncurry (/) . foldl' (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
我将尝试用简单的术语给出解释。正如其他人指出的那样,head 范式不适用于 Haskell,所以我不会在这里考虑它。
正常形式
正常形式的表达式被完全求值,并且不能进一步求值任何子表达式(即它不包含未求值的 thunk)。
这些表达式都是正规形式:
这些表达式不是正规形式:
弱头正规形式
弱头正规形式的表达式已被评估为最外层数据构造函数或 lambda 抽象(head)。子表达式可能已被计算,也可能未被计算。因此,每个范式表达式也是弱头范式,尽管相反的情况通常不成立。
为了判断一个表达式是否是弱头范式,我们只需要查看表达式的最外层部分。如果它是数据构造函数或 lambda,则它是弱头范式。如果它是一个函数应用程序,则不是。
这些表达式采用弱头范式:
如前所述,上面列出的所有范式表达式也采用弱头范式。
这些表达式不是弱头范式:
堆栈溢出
将表达式计算为弱头范式可能需要首先将其他表达式计算为 WHNF。例如,要计算 WHNF 的
1 + (2 + 3)
,我们首先必须计算2 + 3
。如果计算单个表达式导致过多的嵌套计算,则结果是堆栈溢出。当您构建一个大型表达式时,在计算大部分表达式之前,该表达式不会生成任何数据构造函数或 lambda,就会发生这种情况。这些通常是由
foldl
的这种用法引起的:请注意,它必须非常深入才能使表达式变为弱头正常形式。
你可能会想,为什么 Haskell 不提前减少内部表达式呢?那是因为 Haskell 的懒惰。由于一般不能假设每个子表达式都需要,因此表达式是从外到内计算的。
(GHC 有一个严格分析器,可以检测某些总是需要子表达式的情况,然后提前计算它。这然而,这只是一种优化,您不应该依赖它来避免溢出)。
另一方面,这种表达式是完全安全的:
为了避免在我们知道必须计算所有子表达式时构建这些大型表达式,我们希望强制提前计算内部部分。
seq
seq
是一个特殊函数,用于强制计算表达式。它的语义是,seq x y
意味着每当y
被评估为弱头范式时,x
也被评估为弱头范式。它是在
foldl'
的定义中使用的地方,它是foldl
的严格变体。foldl'
的每次迭代都会强制累加器达到 WHNF。因此,它避免构建大型表达式,从而避免堆栈溢出。但正如 HaskellWiki 上的示例所提到的,这并不能在所有情况下都拯救你,因为累加器仅评估为 WHNF。在下面的示例中,累加器是一个元组,因此它只会强制评估元组构造函数,而不是
acc
或len
。为了避免这种情况,我们必须使得对元组构造函数的求值强制对
acc
和len
求值。我们通过使用seq
来做到这一点。I'll try to give an explanation in simple terms. As others have pointed out, head normal form does not apply to Haskell, so I will not consider it here.
Normal form
An expression in normal form is fully evaluated, and no sub-expression could be evaluated any further (i.e. it contains no un-evaluated thunks).
These expressions are all in normal form:
These expressions are not in normal form:
Weak head normal form
An expression in weak head normal form has been evaluated to the outermost data constructor or lambda abstraction (the head). Sub-expressions may or may not have been evaluated. Therefore, every normal form expression is also in weak head normal form, though the opposite does not hold in general.
To determine whether an expression is in weak head normal form, we only have to look at the outermost part of the expression. If it's a data constructor or a lambda, it's in weak head normal form. If it's a function application, it's not.
These expressions are in weak head normal form:
As mentioned, all the normal form expressions listed above are also in weak head normal form.
These expressions are not in weak head normal form:
Stack overflows
Evaluating an expression to weak head normal form may require that other expressions be evaluated to WHNF first. For example, to evaluate
1 + (2 + 3)
to WHNF, we first have to evaluate2 + 3
. If evaluating a single expression leads to too many of these nested evaluations, the result is a stack overflow.This happens when you build up a large expression that does not produce any data constructors or lambdas until a large part of it has been evaluated. These are often caused by this kind of usage of
foldl
:Notice how it has to go quite deep before it can get the expression into weak head normal form.
You may wonder, why does not Haskell reduce the inner expressions ahead of time? That is because of Haskell's laziness. Since it cannot be assumed in general that every subexpression will be needed, expressions are evaluated from the outside in.
(GHC has a strictness analyzer that will detect some situations where a subexpression is always needed and it can then evaluate it ahead of time. This is only an optimization, however, and you should not rely on it to save you from overflows).
This kind of expression, on the other hand, is completely safe:
To avoid building these large expressions when we know all the subexpressions will have to be evaluated, we want to force the inner parts to be evaluated ahead of time.
seq
seq
is a special function that is used to force expressions to be evaluated. Its semantics are thatseq x y
means that whenevery
is evaluated to weak head normal form,x
is also evaluated to weak head normal form.It is among other places used in the definition of
foldl'
, the strict variant offoldl
.Each iteration of
foldl'
forces the accumulator to WHNF. It therefore avoids building up a large expression, and it therefore avoids overflowing the stack.But as the example on HaskellWiki mentions, this does not save you in all cases, as the accumulator is only evaluated to WHNF. In the example below, the accumulator is a tuple, so it will only force evaluation of the tuple constructor, and not
acc
orlen
.To avoid this, we must make it so that evaluating the tuple constructor forces evaluation of
acc
andlen
. We do this by usingseq
.Haskell Wikibooks 中关于Thunks 和弱头范式的部分 对懒惰的描述提供了非常好的描述WHNF 以及这个有用的描述:
The section on Thunks and Weak Head Normal Form in the Haskell Wikibooks description of laziness provides a very good description of WHNF along with this helpful depiction:
Haskell 程序是表达式,它们通过执行求值来运行。
要计算表达式,请将所有函数应用程序替换为其定义。执行此操作的顺序并不重要,但仍然很重要:从最外面的应用程序开始,从左到右进行;这称为惰性求值。
示例:
当没有更多功能应用程序可供替换时,评估停止。结果采用正规形式(或简化正规形式,RNF)。无论您以何种顺序计算表达式,您总是会得到相同的范式(但前提是计算终止)。
对于惰性求值的描述略有不同。也就是说,它表示您应该仅将所有内容评估为弱头正常形式。表达式处于 WHNF 中的情况正好有三种情况:
constructor expression_1 expression_2 ...
(+) 2
或sqrt
\x ->表达式
换句话说,表达式的头部(即最外层函数应用程序)不能被进一步求值,但函数参数可能包含未求值的表达式。
WHNF 示例:
注释
Haskell programs are expressions and they are run by performing evaluation.
To evaluate an expression, replace all function applications by their definitions. The order in which you do this does not matter much, but it's still important: start with the outermost application and proceed from left to right; this is called lazy evaluation.
Example:
Evaluation stops when there are no more function applications left to replace. The result is in normal form (or reduced normal form, RNF). No matter in which order you evaluate an expression, you will always end up with the same normal form (but only if the evaluation terminates).
There is a slightly different description for lazy evaluation. Namely, it says that you should evaluate everything to weak head normal form only. There are precisely three cases for an expression to be in WHNF:
constructor expression_1 expression_2 ...
(+) 2
orsqrt
\x -> expression
In other words, the head of the expression (i.e. the outermost function application) cannot be evaluated any further, but the function argument may contain unevaluated expressions.
Examples of WHNF:
Notes
http://foldoc.org/Weak+Head+Normal+Form Head 给出了很好的解释和示例范式甚至简化了函数抽象内部表达式的位,而“弱”头范式则停止于函数抽象。
从源头来看,如果您有:
那是弱头部正常形式,但不是头部正常形式......因为可能的应用程序被困在尚无法评估的函数内部。
实际的头部范式很难有效地实现。这需要探索函数内部。因此,弱头范式的优点是您仍然可以将函数实现为不透明类型,因此它与编译语言和优化更兼容。
A good explanation with examples is given at http://foldoc.org/Weak+Head+Normal+Form Head normal form simplifies even the bits of an expression inside of a function abstraction, while "weak" head normal form stops at function abstractions.
From the source, if you have:
that is in weak head normal form, but not head normal form... because the possible application is stuck inside of a function that can't be evaluated yet.
Actual head normal form would be difficult to implement efficiently. It would require poking around inside of functions. So the advantage of weak head normal form is that you can still implement functions as an opaque type, and hence it's more compatible with compiled languages and optimization.
WHNF 不希望计算 lambda 的主体,因此
seq 希望其第一个参数位于 WHNF 中,因此
计算
结果为使用 HNF 的值
The WHNF does not want the body of lambdas to be evaluated, so
seq
wants its first argument to be in WHNF, soevaluates to
instead of, what would be using HNF
基本上,假设您有某种 thunk,
t
。现在,如果我们想要将
t
计算为 WHNF 或 NHF(除了函数之外,它们是相同的),我们会发现我们得到类似t1 : t2
的内容,其中t1
和t2
是 thunk。在这种情况下,t1
将是您的0
(或者更确切地说,在没有额外拆箱的情况下,对0
进行重击)seq
和$!
计算 WHNF。注意Basically, suppose you have some sort of thunk,
t
.Now, if we want to evaluate
t
to WHNF or NHF, which are the same except for functions, we would find that we get something liket1 : t2
wheret1
andt2
are thunks. In this case,t1
would be your0
(or rather, a thunk to0
given no extra unboxing)seq
and$!
evalute WHNF. Note that我意识到这是一个老问题,但这里有 WHNF、HNF 和 NF 的明确数学定义。在纯 lambda 演算中:
如果一项的形式为 NF,则该项为 NF
<前><代码>λ x1。 λx2。 ... λ xn。 (x t1 t2 ... tm)
其中
x
是变量,t1, t2, ..., tm
位于 NF 中。如果项的形式为 HNF,则该项属于 HNF
<前><代码>λ x1。 λx2。 ... λ xn。 (x e1 e2 ... em)
其中
x
是变量,e1, e2, ..., em
是任意项。如果一项是 lambda 项
λ x,则该项属于 WHNF。 e
表示任何术语e
或者如果它的形式为其中
x
是变量,e1, e2, ..., em
是任意项。现在考虑一种编程语言,其构造函数
a,b,c...
数量为na, nb, nc...< /code>,这意味着只要
t1, t2, ..., tm
位于 NF 中,则术语a t1 t2 ... tm
其中m = na
是一个 redex,可以是评价。例如,Haskell 中的加法构造函数+
具有元数2
,因为它仅在以正常形式给出两个参数时才进行计算(在本例中) case 整数,它们本身可以被视为无效构造函数)。如果项的形式为 NF,则该项属于 NF
<前><代码>λ x1。 λx2。 ... λ xn。 (x t1 t2 ... tm)
其中
x
是一个变量或一个 arityn
的构造函数,其中m
m
和n
。 nt1, t2, ..., tm
属于 NF。如果项的形式为 HNF,则该项属于 HNF
<前><代码>λ x1。 λx2。 ... λ xn。 (x e1 e2 ... em)
其中
x
是一个变量或一个 arityn
的构造函数,而e1, e2, ... em
是任意项 < em>只要前n
个参数并非全部都在 NF 中。如果满足以下条件,则项在 WHNF 中:它是 lambda 项
λ x。 e
表示任何术语e
或者如果它的形式为其中
x
是一个变量或一个 arityn
的构造函数,而e1, e2, ... em
是任意项 < em>只要前n
个参数并非全部都在 NF 中。特别是,NF 中的任何术语都是 HNF 中的,并且 HNF 中的任何术语都是属于 WHNF,但反之则不然。
I realize this is an old question, but here is an explicit mathematical definition of WHNF, HNF, and NF. In the pure lambda calculus:
A term is in NF if it is of the form
where
x
is a variable, andt1, t2, ..., tm
are in NF.A term is in HNF if it is of the form
where
x
is a variable, ande1, e2, ..., em
are arbitrary terms.A term is in WHNF if it is either a lambda term
λ x. e
for any terme
or if it is of the formwhere
x
is a variable ande1, e2, ..., em
are arbitrary terms.Now consider a programming language with constructors
a,b,c...
of arityna, nb, nc...
, which means that whenevert1, t2, ..., tm
are in NF, then the terma t1 t2 ... tm
wherem = na
is a redex and can be evaluated. For example, the addition constructor+
in Haskell has arity2
, because it only evaluates when it is given two arguments in normal form (in this case integers, which can themselves be considered as nullary constructors).A term is in NF if it is of the form
where
x
is either a variable or a constructor of arityn
withm < n
, andt1, t2, ..., tm
are in NF.A term is in HNF if it is of the form
where
x
is either a variable or a constructor of arityn
, ande1, e2, ... em
are arbitrary terms so long as the firstn
arguments are not all in NF.A term is in WHNF if it is either a lambda term
λ x. e
for any terme
or if it is of the formwhere
x
is either a variable or a constructor of arityn
, ande1, e2, ... em
are arbitrary terms so long as the firstn
arguments are not all in NF.In particular, any term in NF is in HNF, and any term in HNF is in WHNF, but not conversely.
在图简化的实现中,对 HNF 的惰性求值迫使您处理 lambda 演算的名称捕获问题,而对 WHNF 的惰性求值可以让您避免这种情况。
Simon Peyton Jones 的《函数式编程语言的实现》第 11 章。
In an implementation of graph reduction, lazy evaluation to HNF forces you to deal with the name capture problem of lambda calculus, whereas lazy evaluation to WHNF lets you avoid it.
This is explained in Chapter 11 of The Implementation of Functional Programming Languages by Simon Peyton Jones.
Head 范式意味着没有 head redex
(λ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。
Head normal form means there is no head redex
(λx.((λy.y+x)b))b
Reduces to: R, for redex (and yes there's another redex inside it but this is irrelevant). This is a head redex because it's the leftmost redex (the only redex), and there are no lambda terms before it (variables or lambda expressions (applications or abstractions)), only 0 to n abstractors (if R is a redex
(λx.A)B
the abstractor of R isλx
), in this case 0.Because there's a head redex it is not in HNF and it is therefore also not in NF because there's a redex.
WHNF means that it is a lambda abstraction or in HNF. The above is not in HNF and it is not a lambda abstraction, but an application, and is therefore not in WHNF.
λx.((λy.y+x)b)b
is in WHNFIt is a lambda abstraction, but not in HNF because there is a head
λx.Rb
Reduce to
λx.((b+x)b)
. There is no redex therefore it is in normal form.Consider
λx.((λy.zyx)b)
, it simplifies toλx.R
, so it is not in HNF.λx.(k(λy.zyx)b)
simplifies toλx.kR
therefore it is in HNF but not NF.All NF are in HNF and WHNF. All HNF are WHNF.