信頼性はない
都市伝説かもしれん
大体枕詞に特別な理由がなければがつく
思いついたら徐々に更新する
それは違うよってことがあったら @Lugendre まで
Int
であれば値は整数であるという条件だし, Num a => a
であれば Num
クラスのインスタンスである型 a
という条件を表している. newtype
で同じ内部表現を持つ別の型にしてしまうことである.(以下の例は拝借したもの)(拝借元を忘れる1敗)newtype Plain = Plain ByteString
deriving (Show, Eq)
newtype Encrypted = Encrypted ByteString
deriving (Show, Eq)
encrypt :: Plain -> Encrypted
encrypt m = ...
Plain
, Encrypted
両方を受け取るような関数を作る場合,型クラスを持ち出す必要がある.内部表現は一致しているのに型を合わせるためだけに型クラスを持ち出すのはいささか面倒である.data Plain = Plain
data Encrypted = Encrypted
newtype Message a = Message ByteString
deriving (Show, Eq)
encrypt :: Message Plain -> Message Encrypted
encrypt m = ...
Plain
, Encrypted
両方で使えるような関数を定義する際, Message
がパラメトリックなため,わざわざ型クラスを実装する必要ない.append :: Message a -> Message a -> Message a
append x y = coerce $ (coerce @_ @ByteString x) ++ (coerce @_ @ByteString y)
newtype
を使ったときよりも拡張性に優れている. newtype
を用いて新しい型を定義する方法よりも一般に良いとされている. type2.hs
の定義では Plain
や Encrypted
に値コンストラクタがあるが,これらの型は型タグとして使用されることだけを想定しているため,ライブラリ作成者の意図しない使用を制限するためにも値コンストラクタは存在しないほうが良い.幸い,GHCでは EmptyDataDecls
拡張によって値コンストラクタを省略したデータ型宣言をすることができる.これは現在のGHCではデフォルトで有効になっている. type2.hs
の Message
の定義を見てみよう.型変数 a
に入る型は,右側のに現れない.つまり,型変数 a
に何が入ろうが Message
の内部表現が ByteString
であることに変わりはない.このような振る舞いをする型変数を幽霊型 (Phantom type)と呼ぶ.これはこの論文で作られた造語である.論文ではEDSLに静的な型をつけることで,静的に書いた言語の正当性をある程度保証している. type2.hs
の Message
の実装には問題がある.それは期待されない型が型タグとして扱えてしまうことである.つまり, Message Int
は,上記の実装では合法である.我々は, Message
の型タグとして, Plain
と Encrypted
のみを期待しているので,これら以外を受け取らないように制限したい.素朴な考えとして, Message
の型変数 a
に型の型のようなもので注釈をつけられるとよさそうである.そのようなものは種(kind)と呼ばれている. *
と,関数型の種バージョンの k1 -> k2
といった形のものと,制約を表す Constraint
種しか存在しないが,GHC拡張にはデータ型を種へ,値コンストラクタを型へ昇格する DataKinds
という拡張がある.これを使えば,種と,その種を持つ型を定義することができる.
data Status = Plain | Encrypted
deriving (Show, Eq)
Status
種を持つ型, Plain
, Encrypted
と Status
型を持つ値, Plain
, Encrypted
の2つが生成される.ここで注意すべきは,この2つの間になんの関連性も無いことである.型 Plain
, Encrypted
は対応する値コンストラクタは存在しない.つまり,値 Plain
を使っても型 Plain
を作ることはできない.これら2つが生成されるため,型コンストラクタと値コンストラクタを同じ名前にするというHaskellの悪しき風習に従ってデータ型を定義した場合,昇格された値コンストラクタと,元の型コンストラクタがだぶる.この場合,昇格された値コンストラクタの先頭に '
をつけることで区別することができる(e. g. 'Plain
).(昇格された値コンストラクタに '
ついていない場合,GHCはwarningを吐く) Message
の型変数 a
に種注釈をつければよい.デフォルトではこのような注釈はできないが,GHC拡張の KindSignatures
を有効にすれば可能になる.
data Status = Plain | Encrypted
deriving (Show, Eq)
newtype Message (a :: Status) = Message ByteString
deriving (Show, Eq)
Message Int
のような型はコンパイル時に弾かれるようになる. Message
を Vector
に格納することを考えよう.残念ながら Vector
には同じ型のものしかいれることができないため, Message Plain
と Message Excrypted
を一緒に Vector
に入れることはできない.そこで,この2つの Message
型を同一の型として扱うことができないだろうか?こういった要求に答えるのが存在型である.
data SomeMessage = forall a. Show (Message a) => SM (Message a) -- 存在型
hoge :: Vector SomeMessage -> Vector Bool
hoge = ...
SomeMessage
が存在型である.ここで注目すべきは,幽霊型の時は逆に,右辺の型変数 a
が左辺に現れないことである.この型変数を存在量化された型変数と呼び,この型変数により内部表現が変わろうとも型が変わらないということ表している.型変数を存在量化するには forall
キーワードを使って, forall a b.
といったようにする必要がある. SomeMessage
の例のようにコンテキストも一緒に存在型の中に入れておけば,存在型の中の型がどのようなことができるかの情報が残り,コンテキストにある型クラスのメソッドと,それらをつかった多相な関数が扱えるようになる. unsafeCoerce
でガッとやるなども使われたりする. MagicDict
を参照せよ). id
のような中の型に依存しないような関数であれば(意味があるかはおいておいて)適用できるのではなかろうか.次のような関数を書いてみよう.foo :: (a -> b) -> SomeMessage -> b
foo f (SM x) = f x
a
や b
はこの関数の呼び出しのときに具体化されてしまうので, f :: a -> b
は真に任意の型を受け入れられる関数ではないためである.真に多相な型変数を書く手段として, RankNTypes
拡張がある.これを使うと, type7.hs
は以下のようになる.foo :: (forall a. a -> b) -> SomeMessage -> b
foo f (SM x) = f x
a
のスコープはかっこの中に限定される.つまり,型変数 a
は関数の外に影響を受けないことが保証されるため,関数 f
の引数は多相的であることが強いられる.これによって,任意の型を引数に取る多相な関数を表現することができる.