313

ウィークヘッドノーマルフォーム(WHNF)とはどういう意味ですか?ヘッドノーマルフォーム(HNF)とノーマルフォーム(NF)はどういう意味ですか?

実世界のハスケルは次のように述べています。

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

また、Haskellプログラマーがウィーク ヘッドノーマルフォーム(WHNF)を参照しているのを聞くでしょう 。通常のデータの場合、弱い頭の通常の形式は頭の通常の形式と同じです。違いは関数に対してのみ発生し、ここで私たちを気にするのはあまりにも厄介です。

いくつかのリソースと定義(HaskellWikiとHaskellメールリストと無料辞書)を読みましたわかりませ。誰かが例を挙げたり、素人の定義を提供したりできますか?

私はそれが次のようになると推測しています:

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とweakheadnormal form(whnf)を理解している人は、ここで何が悪いのかをすぐに理解できます。 (acc+x, len+1) はすでにwhnfにあるので、 値をwhnfに減らすseq (の定義内の foldl')はこれに対して何もしません。このコードは、元の例と同じようにサンクを構築し foldl ます。それらはタプルの中にあります。解決策は、タプルのコンポーネントを強制することです。

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

--StackoverflowのHaskellWiki

4

9 に答える 9

421

簡単な言葉で説明しようと思います。他の人が指摘しているように、head の正規形は 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

弱頭正常形

弱い head 正規形の式は、最も外側のデータ コンストラクターまたはラムダ抽象化 ( head ) に対して評価されています。サブ式は評価されている場合とされていない場合があります。したがって、すべての正規形表現は弱頭正規形でもありますが、逆は一般には成り立ちません。

式が弱頭正規形であるかどうかを判断するには、式の最も外側の部分を見るだけで済みます。データ コンストラクターまたはラムダの場合は、弱い頭部正規形です。関数アプリケーションの場合は、そうではありません。

これらの式は弱頭正規形です。

(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 は事前に内部式を簡約しないのでしょうか? それは 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れるたびに、も弱い頭部標準形に評価されることを意味します。yx

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以下の例では、accumulator はタプルであるため、 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.
于 2011-07-31T11:59:52.590 に答える
43

Haskell Wikibooksの怠惰の説明のサンクと弱いヘッドの正規形に関するセクションでは、 WHNFの非常に優れた説明と、次の役立つ描写が提供されています。

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

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

于 2012-02-18T16:27:14.477 に答える
29

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. Head Normal Form (HNF) は Haskell には関係ありません。WHNF とは異なり、ラムダ式の本体もある程度評価されます。
于 2011-07-30T15:58:18.453 に答える
26

例を含む適切な説明は、http://foldoc.org/Weak+Head+Normal+Form にあります。頭部標準形は、関数抽象化内の式のビットを単純化しますが、「弱い」頭部標準形は関数抽象化で停止します。 .

ソースから、次のものがある場合:

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

これは弱い頭部標準形ですが、頭部標準形ではありません...可能なアプリケーションがまだ評価できない関数の内部に詰まっているためです。

実際の頭の通常のフォームを効率的に実装するのは難しいでしょう。関数の内部をいじる必要があります。したがって、weak head 正規形の利点は、関数を不透明型として実装できるため、コンパイル済み言語および最適化との互換性が高くなることです。

于 2011-07-29T13:19:00.083 に答える
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
于 2011-07-29T13:20:35.523 に答える
5

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

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

t1 : t2ここでt1t2サンクです。この場合、t1あなた(または、余分な開開0を与えないためのサンク)になります0

seq$!WHNFを評価します。ご了承ください

f $! x = seq x (f x)
于 2011-07-29T13:12:53.383 に答える