我将尝试用简单的术语进行解释。正如其他人指出的那样,头部范式不适用于Haskell,因此在这里我将不考虑它。
正常形式
正常形式的表达式已被完全求值,子表达式无法再求值(即,它不包含未求值的重排)。
这些表达式均为正常形式:
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 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进行求值,因此这并不能在所有情况下为您省钱。在示例中,累加器是一个元组,因此它将仅强制评估元组构造函数,而不是acc
or len
。
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.
为避免这种情况,我们必须做到这一点,以便对元组构造函数的求值强制对acc
和的求值len
。我们使用来做到这一点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.