今回はデータ型を定義するnewtypeとdataの違いについて説明していきます。主に違いを探っていくのでdataやnewtypeの詳しいオプションなどは説明していません。この記事で使用しているGHCのバージョンは8.8.3です。
data
まずはdataです。データ型を定義するためのキーワードです。
data Alpha = A Int
data Beta a = B Int
data Betaの後ろのaは型引数です。これがあるかないかで、カインドが変わってきます。B 10の型はBeta aで、A 10はAlphaになっています。Aは値コンストラクタで、Intを受け取ってAlphaの値を生成します。
Prelude> :k Alpha
Alpha :: *
Prelude> :k Beta
Beta :: * -> *
Prelude> :t A
A :: Int -> Alpha
Prelude> :t B
B :: Int -> Beta a
Prelude> :t A 10
A 10 :: Alpha
Prelude> :t B 10
B 10 :: Beta a
また、フィールド(値の種類)を複数持てます。
data Bool = False | True
data GalileanMoon = Io | Europa | Ganymede | Callisto
BoolはTrueかFalseで、GalileanMoonの型を持つ値は4つの衛星のうちのどれか、ということです。
newtype
newtypeは値コンストラクタが1つで、フィールド(値の種類)も1つだけです。newtypeは常にdataで置き換えられますが、その逆は常に可能ではありません。HLintは、dataで宣言されたそういった(コンストラクタも値も1つの)型を見つけたら「newtypeで置き換えられるけど、どう?」と聞いてくれます。
.\data.hs:2:1: Suggestion: Use newtype instead of data
Found:
data Alpha = A Int
Perhaps:
newtype Alpha = A Int
Note: decreases laziness
先程のAlphaとBetaはnewtypeで置き換えられます。
newtype Alpha = A Int
newtype Beta a = B Int
主な違い
| data | newtype | |
|---|---|---|
| 型引数 | いくつでも | 複数可 |
| レコード構文 | できる | できる |
| フィールド | いくつでも | 1つ |
| 値コンストラクタ | いくつでも | 1つ |
| パフォーマンス | 型による | 基本的にdataより高速 |
評価戦略
newtypeではフィールドは1つしかないため、Alpha _のようなパターンマッチの場合、中身は無視されます。
undefinedは、計算を失敗させて例外を投げる多相定数です。
Prelude> :t undefined
undefined :: a
Prelude> undefined
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries\base\GHC\Err.hs:80:14 in base:GHC.Err
undefined, called at <interactive>:2:1 in interactive:Ghci1
これをdataとnewtypeで実装した別々の型で試してみましょう。
dataの正格性オプション
dataには、値コンストラクタの引数を評価することを強制するオプションがあります。!を値の先頭につけ、それを評価することをコンパイラに強制します。Haskell Wikiの!の部分に簡単な説明があるので読んでみます。
Whenever a data constructor is applied, each argument to the constructor is evaluated if and only if the corresponding type in the algebraic datatype declaration has a strictness flag, denoted by an exclamation point. For example:
data STList a = STCons a !(STList a) -- the second argument to STCons will be -- evaluated before STCons is applied | STNil
値コンストラクタが適用される際に、代数的データ型の宣言が感嘆符であらわされるフラグがついている場合は、各引数がコンストラクタに評価されます。例:
data STList a = STCons a !(STList a) -- STConsの第2引数は -- STConsが適用される前に評価される | STNilto illustrate the difference between strict versus lazy constructor application, consider the following:
stList = STCons 1 undefined lzList = (:) 1 undefined stHead (STCons h _) = h -- this evaluates to undefined when applied to stList lzHead (h : _) = h -- this evaluates to 1 when applied to lzList
正格なコンストラクタと遅延するコンストラクタの違いを説明するには、次の例を考えましょう:
stList = STCons 1 undefined lzList = (:) 1 undefined stHead (STCons h _) = h -- stListに適用すると、undefinedと評価されます lzHead (h : _) = h -- lzListに適用すると、1が返ります。
2つ目の例がわかりやすいですね。STConsは正格なコンストラクタなのでstListの適用中にundefinedを評価しなければならなくなって失敗しますが、lzListはリストの残りを無視して1を返します。
このフラグを使って、dataとnewtypeをテストしてみます。
パターンテスト
ソースコードとテスト関数に結果をコメントしたものです。
data A1 = A1 Int
data A2 = A2 !Int
newtype B1 = B1 Int
f1 = case A1 undefined of
A1 _ -> 1 -- 1
f2 = case A2 undefined of
A2 _ -> 1 -- undefined
f3 = case B1 undefined of
B1 _ -> 1 -- 1
g1 = case undefined of
A1 _ -> 1 -- undefined
g2 = case undefined of
A2 _ -> 1 -- undefined
g3 = case undefined of
B1 _ -> 1 -- 1
i :: Int
i = undefined -- undefined
gInt = case i of
_ -> 1 -- 1
見ると、newtypeで宣言した型は2つのテストでどちらも1を返しています。正格なコンストラクタのA2は、1つ目でコンストラクタの引数を無視するA1に比べてより評価が厳しくなっています。
表にまとめます。
| data | !data | newtype | |
|---|---|---|---|
case C undefined of C _ -> 1 |
1 |
undefined |
1 |
case undefined of C _ -> 1 |
undefined |
undefined |
1 |
例えば、こんな関数があるとき、
function :: B1 -> String
function (B1 _) = "works"
この関数には(B1 undefined)を与えてもちゃんと"works"が返ってきます。newtypeは値の種類が1つしかないので、型を見ればコンストラクタが値の中身を評価せずにパターンマッチを通せるのです。
パフォーマンス1
正格性フラグ(!)を使うとdataのスペースリークは抑えられるようです。2ただし、
If your datatype has a single constructor with a single field, use a
newtypedeclaration instead of adatadeclaration. Thenewtypeconstructor will be optimised away.
もしデータ型が単一のコンストラクタしか持っていないなら、newtypeをdataの代わりに使いましょう。newtypesコンストラクタは最適化されます。
newtypeは型をほどく際のオーバーヘッドが軽減されるため、dataよりも高速になります。
Haskell provides the
newtypekeyword, for the construction of unlifted types. Pattern-matching on a newtype constructor doesn't do any work, ... so every value in the type is wrapped in the constructor.
Haskellはリフトされていない型のために、newtypeキーワードを提供しています。newtypeコンストラクタはパターンマッチでは何もしません。... 全ての型の中の値はコンストラクタに包まれます。
Real World HaskellのWeb版に有識者のコメントがありました。
... The NewType constructor is only used at compilation time, then deleted. ...
... NewTypeコンストラクタはコンパイル時にのみ使われ、削除されます。...
つまりランタイムではnewtypeで包まれた値は裸の値として扱われているわけです。dataと違って複数の値を扱う必要がないため、速くなるわけです。
まとめ
-
dataは値を複数定義できるけど、newtypeは1つだけ -
newtypeはdataよりも使い方の制約が厳しい -
newtypeで定義した型の値は処理系ではコンストラクタを除いた裸の値として扱われている- よって、型コンストラクタは引数を評価されずにパターンマッチが通る
- また、型をほどく際のオーバーヘッドが軽減されて
newtypeは比較的高速になる
感想
すごいH本のnewtypeのパートではあまり解説が詳しくなく、インターネット上でも日本語の文献が少なく、苦労しました。見る限り、HLintを通してみたりソースコードを眺めたりして特別な理由がなければnewtypeで置き換えるのがよいと思います。
参考文献
- Haskell Wiki
- Real World Haskell - Chapter 6. Using Typeclasses
-
newtypeとdataのパフォーマンスの数値的な比較は発見できませんでした。見つかったら追記します。 ↩