簡単な説明をしようと思います。他の人が指摘したように、ヘッドの標準形は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
弱頭正常形
弱い頭の正規形の式は、最も外側のデータコンストラクターまたはラムダ抽象化(頭)に評価されています。部分式は評価されている場合と評価されていない場合があります。したがって、すべての正規形の式も弱い頭の正規形になりますが、一般的に反対は成り立ちません。
式が弱い頭の正規形であるかどうかを判断するには、式の最も外側の部分だけを調べる必要があります。データコンストラクタまたはラムダの場合は、弱い頭の通常の形式です。関数アプリケーションの場合はそうではありません。
これらの表現は弱い頭の正規形です:
(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
ます。単一の式を評価すると、これらのネストされた評価が多すぎる場合、結果はスタックオーバーフローになります。
これは、その大部分が評価されるまで、データコンストラクターやラムダを生成しない大きな式を作成すると発生します。これらは多くの場合、この種の使用法によって引き起こされます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は事前に内部式を削減しないのですか?それはハスケルの怠惰のためです。一般に、すべての部分式が必要であるとは想定できないため、式は外部から評価されます。
(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'
、の厳密な変形であるの定義で使用される他の場所の1つです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
andの評価が強制されるようにする必要があり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.