web-dev-qa-db-ja.com

Haskellに「データ」と「newtype」があるのはなぜですか?

newtype定義は、いくつかの制限(たとえば、1つのコンストラクターのみ)に従う単なるdata定義であり、これらの制限により、ランタイムシステムはnewtypesを処理できるようです。より効率的に。また、未定義の値のパターンマッチングの処理は少し異なります。

しかし、Haskellはdataの定義のみを知っていて、newtypesを知らないと仮定します。コンパイラは、特定のデータ定義がこれらの制限に従っているかどうかを自分で判断できず、それをより効率的に自動的に処理できますか?

私は何かを逃していると確信しています。これにはもっと深い理由があるはずです。

142
martingw

newtypeと単一コンストラクタdataは両方とも単一の値コンストラクタを導入しますが、newtypeによって導入された値コンストラクタは厳密であり、dataによって導入された値コンストラクタは厳格です怠け者です。あなたが持っているなら

data D = D Int
newtype N = N Int

その後、N undefinedundefinedと同等であり、評価時にエラーが発生します。しかし、D undefinednotundefinedと同等であり、内部を覗かない限り評価できます。

コンパイラはこれを自分で処理できませんでした。

いいえ、そうではありません。これは、コンストラクターが厳格か怠かをプログラマーが判断できる場合です。コンストラクターを厳密または遅延にするタイミングと方法を理解するには、遅延評価について、私よりもはるかによく理解する必要があります。レポートのアイデアに固執します。つまり、いくつかの異なる互換性のない測定値を持つように、既存のタイプの名前を変更するためにnewtypeがあるということです。

newtype Feet = Feet Double
newtype Cm   = Cm   Double

どちらも実行時にDoubleとまったく同じように動作しますが、コンパイラはこれらを混同させないことを約束します。

175
Norman Ramsey

Learn Has a Haskell によると:

Dataキーワードの代わりに、newtypeキーワードが使用されます。それはなぜですか? 1つは、newtypeの方が高速です。 dataキーワードを使用して型をラップすると、プログラムの実行中にすべてのラップとアンラップにオーバーヘッドが発生します。しかし、newtypeを使用する場合、Haskellは既存のタイプを新しいタイプにラップするために使用していることを認識します(そのため、名前が同じです)。それを念頭に置いて、Haskellは、どの値がどのタイプであるかを解決したら、ラップとアンラップを取り除くことができます。

では、なぜデータの代わりに常にnewtypeを使用しないのですか?さて、newtypeキーワードを使用して既存の型から新しい型を作成する場合、値コンストラクターを1つだけ持つことができ、その値コンストラクターはフィールドを1つだけ持つことができます。しかし、データを使用すると、複数の値コンストラクターを持つデータ型を作成でき、各コンストラクターはゼロ個以上のフィールドを持つことができます。

data Profession = Fighter | Archer | Accountant  

data Race = Human | Elf | Orc | Goblin  

data PlayerCharacter = PlayerCharacter Race Profession 

Newtypeを使用する場合、1つのフィールドを持つ1つのコンストラクターに制限されます。

次に、次のタイプを検討します。

data CoolBool = CoolBool { getCoolBool :: Bool } 

Dataキーワードで定義されたのは、ごく普通の代数データ型です。値コンストラクターが1つあり、値コンストラクターには型がBoolのフィールドが1つあります。 CoolBoolでパターンが一致し、CoolBool内のBoolがTrueかFalseかに関係なく、値「hello」を返す関数を作成してみましょう。

helloMe :: CoolBool -> String  
helloMe (CoolBool _) = "hello"  

この関数を通常のCoolBoolに適用する代わりに、曲線ボールを投げて未定義に適用しましょう!

ghci> helloMe undefined  
"*** Exception: Prelude.undefined  

いいね!例外!なぜこの例外が発生したのですか? dataキーワードで定義された型は、複数の値コンストラクターを持つことができます(CoolBoolには1つしかありませんが)。そのため、関数に与えられた値が(CoolBool _)パターンに準拠しているかどうかを確認するために、Haskellは値を作成したときにどの値コンストラクターが使用されたかを確認するのに十分な値を評価する必要があります。そして、未定義の値を少しでも評価しようとすると、例外がスローされます。

CoolBoolのデータキーワードを使用する代わりに、newtypeを使用してみましょう。

newtype CoolBool = CoolBool { getCoolBool :: Bool }   

Newtypeまたはdataを使用して型を定義する場合、パターンマッチング構文は同じであるため、helloMe関数を変更する必要はありません。ここでも同じことを行い、未定義の値にhelloMeを適用してみましょう。

ghci> helloMe undefined  
"hello"

出来た!うーん、なぜですか?前述のように、newtypeを使用すると、Haskellは元の値と同じ方法で新しい型の値を内部的に表現できます。それらの周りに別のボックスを追加する必要はありません。異なるタイプの値に注意するだけです。また、Haskellはnewtypeキーワードで作成された型にはコンストラクタが1つしかないことを知っているため、newtype型には1つしか設定できないため、関数に渡される値を評価して(CoolBool _)パターンに準拠していることを確認する必要はありません可能な値コンストラクタと1つのフィールド!

この動作の違いはささいなように思えるかもしれませんが、データとnewtypeで定義された型は両方とも値コンストラクターとフィールドを持っているため、プログラマーの観点からは同様に動作しますが、実際には2つの異なるメカニズムであることに気付くので、実際には非常に重要です。データを使用して独自のタイプをゼロから作成することができますが、newtypeは既存のタイプから完全に新しいタイプを作成するためのものです。 newtype値のパターンマッチングは、(データを使用する場合のように)ボックスから何かを取り出すようなものではなく、あるタイプから別のタイプへの直接変換に関するものです。

別のソースがあります。 このNewtypeの記事 によると:

Newtype宣言は、データとほぼ同じ方法で新しい型を作成します。 newtypesの構文と使用法は、データ宣言とほぼ同じです。実際、newtypeキーワードをデータで置き換えることができ、それでもコンパイルできます。実際、プログラムが動作する可能性は十分にあります。ただし、その逆は当てはまりません。型に含まれるコンストラクタが1つだけで、フィールドが1つだけである場合にのみ、データをnewtypeに置き換えることができます。

いくつかの例:

newtype Fd = Fd CInt
-- data Fd = Fd CInt would also be valid

-- newtypes can have deriving clauses just like normal types
newtype Identity a = Identity a
  deriving (Eq, Ord, Read, Show)

-- record syntax is still allowed, but only for one field
newtype State s a = State { runState :: s -> (s, a) }

-- this is *not* allowed:
-- newtype Pair a b = Pair { pairFst :: a, pairSnd :: b }
-- but this is:
data Pair a b = Pair { pairFst :: a, pairSnd :: b }
-- and so is this:
newtype Pair' a b = Pair' (a, b)

かなり限られているようです!では、なぜだれもがnewtypeを使用するのでしょうか?

短いバージョン1つのフィールドを持つ1つのコンストラクターの制限は、新しいタイプとフィールドのタイプが直接対応することを意味します。

State :: (s -> (a, s)) -> State s a
runState :: State s a -> (s -> (a, s))

または数学的には同型です。つまり、コンパイル時に型をチェックした後、実行時に2つの型を基本的に同じように扱うことができ、データコンストラクターに通常関連付けられるオーバーヘッドや間接性はありません。したがって、特定の型に対して異なる型クラスインスタンスを宣言する場合、または型を抽象化する場合は、newtypeでラップすることができ、型チェッカーとは異なるが、実行時には同一と見なされます。その後、GHCが理由なくバイトバケットをシャッフルすることを心配することなく、ファントム型または再帰型などのあらゆる種類のディープトリックを使用できます。

乱雑なビットについては 記事 を参照してください...

59
Rose Perrone

箇条書きリストに夢中な人向けのシンプルなバージョン(見つけられなかったため、自分で作成する必要があります):

data-値コンストラクターを使用して新しい代数型を作成します

  • 複数の値コンストラクターを持つことができます
  • 値コンストラクターは遅延しています
  • 値には複数のフィールドを含めることができます
  • コンパイルとランタイムの両方に影響し、ランタイムのオーバーヘッドがあります
  • 作成されたタイプは別個の新しいタイプです
  • 独自のタイプクラスインスタンスを持つことができます
  • 値コンストラクターに対するパターンマッチングの場合、少なくとも弱い頭部正規形(WHNF)に評価されます*
  • 新しいデータ型の作成に使用(例:Address {Zip :: String、street :: String})

newtype-値コンストラクターで新しい「装飾」タイプを作成します

  • 値コンストラクターを1つだけ持つことができます
  • 値コンストラクターは厳密です
  • 値に含めることができるフィールドは1つだけです
  • コンパイルのみに影響し、実行時のオーバーヘッドはありません
  • 作成されたタイプは別個の新しいタイプです
  • 独自のタイプクラスインスタンスを持つことができます
  • 値コンストラクターに対するパターンマッチングの場合、CANはまったく評価されません*
  • サポートされている操作の異なるセットを持つ既存のタイプに基づいて、または元のタイプと交換できないより高いレベルの概念を作成するために使用されます(例:Meter、Cm、Feet is Double)

type-タイプの代替名(同義語)を作成します(Cのtypedefなど)

  • 値コンストラクタなし
  • フィールドなし
  • コンパイルのみに影響し、実行時のオーバーヘッドはありません
  • 新しいタイプは作成されません(既存のタイプの新しい名前のみ)
  • 独自の型クラスインスタンスを持つことはできません
  • データコンストラクターに対するパターンマッチングの場合、元の型と同じように動作します
  • サポートされている操作の同じセットを持つ既存のタイプに基づいて、より高いレベルの概念を作成するために使用されます(例:文字列は[Char])

[*]パターンマッチング遅延について:

data DataBox a = DataBox Int
newtype NewtypeBox a = NewtypeBox Int

dataMatcher :: DataBox -> String
dataMatcher (DataBox _) = "data"

newtypeMatcher :: NewtypeBox -> String 
newtypeMatcher (NewtypeBox _) = "newtype"

ghci> dataMatcher undefined
"*** Exception: Prelude.undefined

ghci> newtypeMatcher undefined
“newtype"
45
wonder.mice

私の頭の上から。データ宣言では、「メンバー」のアクセスとストレージで遅延評価が使用されますが、newtypeでは使用されません。また、Newtypeはコンポーネントから以前のすべてのタイプインスタンスを取り除き、その実装を効果的に隠します。一方、データは実装を開いたままにします。

複雑なデータ型でボイラープレートコードを使用しない場合は、新しい型を使用する傾向があります。これにより、コンパイルと実行の両方が高速化され、新しい型が使用されるコードの複雑さが軽減されます。

これについて最初に読んだとき、私は この章 Haskellへの穏やかな入門のかなり直感的であることに気付きました。

9
Dan