web-dev-qa-db-ja.com

弱い頭の標準形とは何ですか?

Weak Head Normal Form(WHNF)はどういう意味ですか? Head Normal form(HNF)およびNormal Form(NF)平均?

Real World Haskell 状態:

おなじみのseq関数は、式を「head normal form」(略してHNF)と呼ぶものに評価します。最外部のコンストラクター(「ヘッド」)に到達すると停止します。これは、式が完全に評価される標準形式(NF)とは異なります。

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

いくつかのリソースと定義を読みました( Haskell Wiki および Haskell Mail List および Free Dictionary )ですが、わかりません。誰かが例を挙げたり、素人の定義を提供したりできますか?

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

WHNF = thunk : thunk

HNF = 0 : thunk 

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

seq($!)はWHNFとHNFにどのように関係しますか?

更新

私はまだ混乱しています。 HNFを無視するという回答がいくつかあることは知っています。さまざまな定義を読むと、WHNFとHNFの通常のデータに違いはないようです。ただし、機能に関しては違いがあるように見えます。違いがなかった場合、なぜfoldl'seqが必要なのですか?

混乱のもう1つのポイントは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は値をwhnfに減らしますが、これには何もしません。このコードは、元のfoldlの例のようにサンクを構築します。それらはTuple内にあります。解決策は、タプルのコンポーネントを強制することです。

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

- StackoverflowのHaskell Wiki

278
user295190

私は簡単な言葉で説明しようとします。他の人が指摘したように、頭の標準形は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)に評価されています。部分式評価されている場合とされていない場合がある。したがって、すべての正規形の表現も弱い頭部の正規形になりますが、一般的に反対は成り立ちません。

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

これらの式は、弱い頭の標準形です。

(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の怠ofのためです。一般に、すべての部分式が必要になるとは想定できないため、式は外部から評価されます。

(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'の定義で使用される他の場所の1つです。

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または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.

これを回避するには、Tupleコンストラクターの評価で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.
384
hammar

Haskell Wikibooksの Thunks and Weak Head Normal Form のセクション lazinessの説明 は、この有用な描写とともにWHNFの非常に良い説明を提供します。

Evaluating the value (4, [1, 2]) step by step. The first stage is completely unevaluated; all subsequent forms are in WHNF, and the last one is also in normal form.

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

41
aculich

Haskellプログラムはexpressionsであり、evaluationを実行することで実行されます。

式を評価するには、すべての関数アプリケーションを定義で置き換えます。これを行う順序は重要ではありませんが、それでも重要です。最も外側のアプリケーションから始めて、左から右に進みます。これは、遅延評価と呼ばれます。

例:

   take 1 (1:2:3:[])
=> { apply take }
   1 : take (1-1) (2:3:[])
=> { apply (-)  }
   1 : take 0 (2:3:[])
=> { apply take }
   1 : []

置換する関数アプリケーションがなくなると、評価は停止します。結果はnormal form(またはreduced normal form、RNF)です。式を評価する順序に関係なく、常に同じ正規形になります(ただし、評価が終了する場合のみ)。

遅延評価にはわずかに異なる説明があります。つまり、すべてをweak head normal formのみに評価する必要があるということです。 WHNFにある式には、正確に3つのケースがあります。

  • コンストラクター:constructor expression_1 expression_2 ...
  • (+) 2sqrtなどの引数が少なすぎる組み込み関数
  • ラムダ式:\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とは異なります。
27

http://foldoc.org/Weak+Head+Normal+Form での適切な説明が提供されています。頭の標準形は関数の抽象化で停止します。

あなたが持っている場合、ソースから:

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

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

実際の頭の標準形は、効率的に実装するのが難しいでしょう。関数の内部を突く必要があります。したがって、弱い頭の標準形の利点は、関数を不透明(OPAQUE)型として実装できることです。したがって、コンパイルされた言語および最適化との互換性が高くなります。

26
Chris Smith

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
12
marc

基本的に、何らかのサンクtがあるとします。

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

t1 : t2ここで、t1およびt2はサンクです。この場合、t10になります(または、追加のボックス化解除なしで0へのサンク)

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

f $! x = seq x (f x)
5
alternative