存在量化(和訳)
型の中で存在量化された型変数を許す.
データ型宣言での存在量化子の利用のアイデアはPerryによって提案され,Hopeらによって実装された.その後,LauferとOderskyによって形式化された.Lennart AugustssonのhbcというHaskellコンパイラで数年使われ,非常に有用と判明した.ここにそのアイデアを書く.次の宣言を考えてみよう.
code: EQ1.hs
data Foo = forall a. MkFoo a (a -> Bool)
| Nil
データ型Fooには,次の型を持つ2つの型コンストラクタがある.
code: EQ2.hs
MkFoo :: forall a. a -> (a -> Bool) -> Foo
Nil :: Foo
MkFoo型の型変数aはデータ型Fooには現れない.例えば,次の式は問題ない.
code: EQ3.hs
ここで,(MkFoo 3 even)は整数をBoolにマップする関数evenと整数をパッケージングする.加えて,(MkFoo 'c' isUpper)は,文字とそれを使う関数をパッケージングしている(訳注:ここでのパッケージングとは関数と整数の情報をまとめているということ).これら2つはそれぞれFoo型であり,単一のリストに入れることができる.
我々はFoo型の値で何ができるだろうか?特に,MkFooでパターンマッチングをするとどうなるだろうか?
code: EQ4.hs
f (MkFoo val fn) = ???
我々がvalとfnについて知ってることはその2つの間に互換性があるということだけなので,我々ができそうな唯一の意味のあることはfnをvalに適用して,Bool値を得ることだけである.例えばこんな感じ:
code: EQ5.hs
f :: Foo -> Bool
f (MkFoo val fn) = fn val
これにより,異種の値をそれらを操作する一連の関数とともにパッケージングし,そのパッケージの集まりを同じ方法で取り扱うことができる.存在型でこのようにOOPのようなプログラミング(ダック・タイピング)をかなり表現することができる.
なぜ存在か?
これは存在量化とどこが関係しているだろうか?単に,MkFooが以下とほぼ同型の型を持っているからである.
code: EQ6.hs
MkFoo :: (exists a. (a, a -> Bool)) -> Foo -- 擬似コード
しかし,Haskellプログラマは前述したように,普通の全称量化された型を安全に考えることができ,それによって新しい存在量化された構造を追加することを避けることができる.
存在と型クラス
簡単な拡張はコンストラクタの前に任意のコンテキストを許すことだ.例えば次のようになる.
code: EQ7.hs
data Baz = forall a. Eq a => Baz1 a a
| forall b. Show b => Baz2 b (b -> b)
2つのコンストラクタは期待通り次のような型を持つ.
code: EQ8.hs
Baz1 :: forall a. Eq a => Baz1 a a -> Baz
Baz2 :: forall b. Show b => Baz2 b (b -> b) -> Baz
しかし,Baz1でパターンマッチングをすると,マッチした値を同値比較でき,Baz2でパターンマッチングすると最初にマッチした値を文字列に(関数を適用できるのはもちろん)変換することができる.故に次のプログラムは合法である.
code: EQ9.hs
f :: Baz -> String
f (Baz1 p q) | p == q = "Yes"
| otherwise = "No"
f (Baz2 v fn) = show (fn v)
操作的には,辞書渡しによる型クラスの実装では,コンストラクタBaz1とBaz2はそれぞれEqとShowの辞書を格納し,パターンマッチングでそれらを取り出す必要がある.
レコードのコンストラクタ
GHCはレコード構文とともに存在型を使用することも許す.例えば,以下のようなコードが書ける.
code: EQ10.hs
data Counter a = forall self. NewCounter
{ _this :: self
, _inc :: self -> self
, _display :: self -> IO()
, tag :: a
}
ここで,tagはパブリックフィールドであり,型付セレクタ関数tag :: Counter a -> aをもつ.self型は外から隠されており,_this, _inc, _displayを関数として適用しようとするとコンパイル時エラーが発生する.言い換えれば,存在量化された型変数を使わないような型を持つフィールドのみレコードセレクタ関数を生成する.(この例では,レコードセレクタが定義されていないフィールドの名前はアンダースコアから始めているが,これはただのプログラミングスタイルであり,GHCはこれを無視する.)
これらの隠しフィールドを利用するには,いくつかのヘルパー関数を作成する必要がある.
code: EQ11.hs
inc ::Counter a -> Counter a
inc (NewCounter x i d t) = NewCounter
{ _this = i x, _inc = i, _display = d, tag t }
display :: Counter a -> IO ()
display NewCounter{ _this = x, _display = d } = d x
これにより,様々な内部的に異なる実装をもつカウンタを定義できる.
code: EQ12.hs
counterA :: Counter String
counterA = NewCounter
{ _this = 0, _inc = (1+), _display = print, tag = "A" }
counterB :: Counter String
counterB = NewCounter
{ _this = "", _inc = ('#':), _display = putStrLn, tag = "B" }
main = do
display (inc counterA) -- print "1"
display (inc (inc counterB)) -- print "##"
存在型(とGADT)に対するレコードの更新構文はサポートされている.
code: EQ13.hs
seTag :: Counter a -> a -> Counter a
seTag obj t = obj{ tag= t}
レコード更新の規則は以下の通りである.
更新されたフィールドの型はデータコンストラクタの全称量化された型変数のみを記述できる.GADTsの場合,フィールドには,コンストラクタの結果の型に単純な型変数引数として表示される型だけが記述される.
例としては,
code: EQ14.hs
data T a b where { T1 { f1 :: a, f2 :: b, f3 :: (b, c) } :: T a b } -- cは存在量化された型変数
upd1 t x = t { f1 = x } -- OK: upd1 :: T a b -> a' -> T a' b
upd2 t x = t { f3 = x } -- BAD (f3の型にはcが記述されており,cは存在量化された型変数であるため)
data G a b where { G1 { g1::a, g2::c } :: G a c } upd3 g x = g { g1 = x } -- OK: upd3 :: G a b -> a' -> G a' b
upd4 g x = g { g2 = x } -- BAD (g2の型にはcが記述されおり,これはG1の結果の型の単純な型変数引数ではない)
制限事項
存在量化されたコンストラクタを使用する上ではいくつかの制限がある
パターンマッチングする時,各パターンマッチでは,存在量化された型変数ごとに異なる新しい型が導入される.これらの型は他の型と統一することも,パターンマッチのスコープから逃げることもできない.例えば次のコードは正しくない.
code: EQ15.hs
f1 (MkFoo a f) = a
ここでは,MkFooによって束縛された型は「逃げて」しまう.なぜなら,aはf1の結果だからである.なぜこれが間違ってるかを理解する一つの方法は,f1がどんな型を持っているかを調べることである.
code: EQ16.hs
f1 :: Foo -> a -- 奇妙な型
結果の型でこのaとはなんだろうか? 明らかに以下を表すものではないだろう.
code: EQ17.hs
f1 :: forall a. Foo -> a -- 間違っている
これはただ,元のプログラムが明白に間違っているだけである.他のエラーとして次のコードが有る.
code: EQ18.hs
f2 (Baz1 a b) (Baz1 p q) = a == q
a == bやp == qは可能だが,a == qはBaz1コンストラクタから作られる2つの異なる型を等号で結んでいるので間違っている.
letまたはwhereの束縛の中の存在量化されたコンストラクタに対してパターンマッチングを行うことはできない.よって次は違法である.
code: EQ19.hs
f3 x = a == b where { Baz1 a b = x }
代わりにcase式を使えば良い.
code: EQ20.hs
f3 x = case x of Baz1 a b -> a == b
一般に,case式か,関数定義パターンでのみ,存在量化されたコンストラクタによるパターンマッチングが可能になる.この制限の理由は,実際は実装のせいである.束縛群の型検査 は,事態を複雑にしている存在量化が無い場合であっても既に悪夢である.さらに,モジュールのトップレベルでの存在型のパターン束縛は意味がない.なぜなら,存在量化された型が「逃げてしまう」のを妨ぐ明確な方法がないからである.だから今のところ,言葉で簡単に表現できる制約がある.それがいかに腹立たしいかすぐ理解できるだろう.
newtype宣言には存在量化は使用できない.よって次のコードは違法である.
code: EQ21.hs
newtype T = forall a. Ord a => MkT a
理由:型Tの値はOrd tの辞書と型tの値のペアとして表す必要がある.これは,newtypeには具体的な表現がないという考えと矛盾する.newtypeの代わりにdataを使うことで,同じ効率と効果を得ることができる.オーバーロードが含まれていない場合,dataは実装コストを持つため,存在量化されたnewtypeを許可する言い分もあるわけだが,そもそも単一フィールドの存在量化されたコンストラクタはそれほど役に立たない.故に,それを変更する説得力のある理由がない限り,この単純な制限は有効である.
存在量化されたデータコンストラクタのデータ型のインスタンスをderivingで導出することはできない.殆どの場合,それは意味を成さないからである.例えば次のコードを考えよう.
code: EQ22.hs
data T = forall a. MkT a deriving ( Eq ) 通常の方法でEqを導出するには,2つのMkTコンストラクタの単一のコンポーネントが等しい必要がある.
code: EQ22.hs
instance Eq T where
(MkT a) == (MkT b) = ???
しかしながら,aとbは明確に違う型で,故に比較できない.導出したインスタンスが意味を成す例を想像することは可能だが,単純にそのような宣言を禁止する方が簡単である.自分自身で独自のインスタンスを定義しましょう.