ウィークヘッドノーマルフォームとは


290

何をしない弱い頭正規形(WHNFは)意味ですか?何が頭ノーマルフォーム(HNF)と正規形(NF)の平均?

現実世界のHaskell次のように述べています:

おなじみのseq関数は、式を評価して、headノーマルフォーム(HNFと略記)と呼びます。最も外側のコンストラクタ(「ヘッド」)に到達すると停止します。これは、式が完全に評価される正規形(NF)とは異なります。

また、Haskellプログラマーが弱頭標準形(WHNF)に言及していることを聞くでしょう。通常のデータの場合、弱い頭部の正規形は頭部の正規形と同じです。違いは関数に対してのみ発生し、ここで私たちを心配するのはあまりにも厄介です。

いくつかのリソースと定義(Haskell WikiHaskell Mail ListFree Dictionary)を読みましたが、理解できません。誰かが例を挙げたり、素人の定義を提供したりできますか?

私はそれが次のようになると思います:

WHNF = thunk : thunk

HNF = 0 : thunk 

NF = 0 : 1 : 2 : 3 : []

WHNFとHNF はどのようseq($!)関連していますか?

更新

私はまだ混乱しています。HNFを無視するという回答もいくつかあります。さまざまな定義を読むと、WHNFとHNFの通常のデータに違いはないようです。ただし、関数に関しては違いがあるようです。違いがなければ、なぜseq必要なのfoldl'ですか?

混乱のもう1つのポイントは、Haskell Wikiからです。これは、seqWHNFに換算すると述べており、次の例には何もしません。次にseq、評価を強制するために使用する必要があると言います。それはそれをHNFに強制していませんか?

一般的な初心者スタックオーバーフローコード:

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

seqと弱い頭の正常な形式(whnf)を理解している人は、ここで何がうまくいかないかをすぐに理解できます。(acc + x、len + 1)はすでにwhnfにあるため、値をwhnfに減らすseqはこれに対して何もしません。このコードは、元のfoldlの例と同様にサンクを構築します。サンクはタプル内にあります。解決策は、タプルのコンポーネントを強制することです。

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

- StackOverflowの上ハスケルウィキ


1
一般に、WHNFとRNFについて話します。(RNFは、あなたがNFを呼んでいる)
代替

5
@monadic RNFのRは何を表していますか?
dave4420

7
@ dave4420:削減
マルク・

回答:


399

簡単な説明をしようと思います。他の人が指摘したように、ヘッドの標準形は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に対してのみ評価されるため、すべてのケースでこれが役立つわけではありません。この例では、アキュムレータはタプルであるため、タプルコンストラクタの評価のみを強制し、accor は強制しません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.

これを回避するには、タプルコンストラクターの評価でaccandの評価が強制されるようにする必要があり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.

31
頭の標準形は、ラムダの本体も削減する必要がありますが、弱い頭の標準形にはこの要件がありません。\x -> 1 + 1WHNFもそうですが、HNFはそうではありません。
ハマー

ウィキペディアは、HNFが「頭の位置にベータ-redexがない場合、用語は頭の標準形である」と述べています。Haskellはベータ式の部分式を使わないので「弱い」のでしょうか?

厳密なデータコンストラクターはどのように機能しますか?彼らはちょうどseq彼らの議論を要求するようなものですか?
ベルギ2014年

1
@CaptainObvious:1 + 2はNFでもWHNFでもありません。式は常に正規形ではありません。
ハンマー

2
@Zorobay:結果を出力するために、GHCiは式をWHNFだけでなく完全にNFに評価します。2つのバリアントの違いを確認する1つの方法は、でメモリ統計を有効にすること:set +sです。これで、がfoldl' fより多くのサンクを割り当てることになりfoldl' f'ます。
Hammar 2017年

43

Haskell Wikibooks の怠惰の説明の「Thunks and Weak Head Normal Form」のセクションでは、WHNFの非常に優れた説明とこの役立つ描写を提供しています。

値(4、[1、2])を段階的に評価します。 最初の段階は完全に評価されていません。 後続のすべての形式はWHNFであり、最後の形式も通常の形式です。

値(4、[1、2])を段階的に評価します。最初の段階は完全に評価されていません。後続のすべての形式はWHNFであり、最後の形式も通常の形式です。


5
私は人々が頭の標準形を無視すると言っているのを知っていますが、その図の例で頭の標準形がどのようになっているのか教えていただけますか?
CMCDragonkai 2015

28

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に含まれるケースは3つあります。

  • コンストラクタ: constructor expression_1 expression_2 ...
  • (+) 2またはなどの引数が少なすぎる組み込み関数sqrt
  • ラムダ式: \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. 時々、人々は評価されていない表現を「サンク」と呼んでいますが、それを理解するのに良い方法だとは思いません。
  3. 頭の正規形(HNF)はHaskellには関係ありません。WHNFとは異なり、ラムダ式の本体もある程度評価されます。

使用であり、seqfoldl'力がWHNFからHNFへの評価?

1
@snmcdonald:いいえ、HaskellはHNFを使用しません。評価seq expr1 expr2するとexpr1、2番目の式を評価する前に、最初の式がWHNFに評価されますexpr2
ハインリッヒアフェルムス

26

http://foldoc.org/Weak+Head+Normal+Formでの適切な説明はhttp://foldoc.org/Weak+Head+Normal+Formにあります。Headの 標準フォームは、関数の抽象化の内部の式のビットでさえも単純化します。 。

ソースから:

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

それは弱いヘッドの通常の形式ですが、ヘッドの通常の形式ではありません...

実際の頭部の標準形式は、効率的に実装するのが難しいでしょう。関数の内部をざっと見る必要があります。したがって、弱いヘッドの正規形の利点は、関数を不透明な型として実装できるため、コンパイルされた言語と最適化との互換性が高いことです。


12

WHNFはラムダの本体の評価を望まないため、

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

または、例を誤解している、またはWHNFとHNFで1と2を混ぜている。
Zhen

5

基本的に、ある種のサンクがあるとしますt

ここで、t関数を除いて同じであるWHNFまたはNHF に評価する場合、次のような結果が得られます。

t1 : t2どこt1t2サンクです。この場合、t1あなたの0(またはむしろ、0余分なボックス化解除を与えられなかったことに感謝します)

seqそして$!evalute WHNF。ご了承ください

f $! x = seq x (f x)

1
@snmcdonald HNFを無視します。seqは、これがWHNFに評価される場合、WHNFの最初の引数を評価すると述べています。
代替
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.