11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Haskellのnewtypeとdataの違い

Last updated at Posted at 2020-06-05

今回はデータ型を定義するnewtypedataの違いについて説明していきます。主に違いを探っていくのでdatanewtypeの詳しいオプションなどは説明していません。この記事で使用しているGHCのバージョンは8.8.3です。

data

まずはdataです。データ型を定義するためのキーワードです。

data Alpha = A Int
data Beta a = B Int

data Betaの後ろのa型引数です。これがあるかないかで、カインドが変わってきます。B 10の型はBeta aで、A 10Alphaになっています。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

BoolTrueFalseで、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

先程のAlphaBetanewtypeで置き換えられます。

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

これをdatanewtypeで実装した別々の型で試してみましょう。

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が適用される前に評価される
        | STNil

to 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を返します。

このフラグを使って、datanewtypeをテストしてみます。

パターンテスト

ソースコードとテスト関数に結果をコメントしたものです。


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 newtype declaration instead of a data declaration. The newtype constructor will be optimised away.
もしデータ型が単一のコンストラクタしか持っていないなら、newtypedataの代わりに使いましょう。newtypesコンストラクタは最適化されます。

newtypeは型をほどく際のオーバーヘッドが軽減されるため、dataよりも高速になります。

Haskell provides the newtype keyword, 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つだけ
  • newtypedataよりも使い方の制約が厳しい
  • newtypeで定義した型の値は処理系ではコンストラクタを除いた裸の値として扱われている
    • よって、型コンストラクタは引数を評価されずにパターンマッチが通る
    • また、型をほどく際のオーバーヘッドが軽減されてnewtypeは比較的高速になる

感想

すごいH本のnewtypeのパートではあまり解説が詳しくなく、インターネット上でも日本語の文献が少なく、苦労しました。見る限り、HLintを通してみたりソースコードを眺めたりして特別な理由がなければnewtypeで置き換えるのがよいと思います。

参考文献

  1. newtypedataのパフォーマンスの数値的な比較は発見できませんでした。見つかったら追記します。

  2. Performance/Data typesより。

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?