添字付けされた型の族は、型水準のプログラミングを容易にするための拡張である。型の族は、関連データ型(“Associated Types with Class”, M. Chakravarty, G. Keller, S. Peyton Jones, and S. Marlow. In Proceedings of “The 32nd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL'05)”, pages 1-13, ACM Press, 2005)および関連型シノニム(“Type Associated Type Synonyms”. M. Chakravarty, G. Keller, and S. Peyton Jones. In Proceedings of “The Tenth ACM SIGPLAN International Conference on Functional Programming”, ACM Press, pages 241-253, 2005)の一般化である。型の族自体は論文“Type Checking with Open Type Functions”, T. Schrijvers, S. Peyton-Jones, M. Chakravarty, and M. Sulzmann, in Proceedings of “ICFP 2008: The 13th ACM SIGPLAN International Conference on Functional Programming”, ACM Press, pages 51-62, 2008 Functional Programming”, ACM Press, pages 241-253, 2005)に記述されている。型の族は、要するに、型で添字付けされたデータ型と、型についての名前の付いた関数を提供する。これらは、総称(generic)プログラミングや高度にパラメタ化されたライブラリインタフェース、さらには、依存型と同様に、インタフェースにより多くの静的な情報を与えるのに有用である。また、型の族は関数従属の代わりとなるものとも見なされている。関数従属では型水準のプログラミングを関係的に行うのに対して、型の族はより関数的な様式を可能にする。
添字付けされた型の族(indexed type family)、短くいうと型族(type family)は、型の集合を表すような型構築子である。集合の要素は、型族構築子に型引数(型添字と呼ぶ)を与えることで記述される。通常のパラメタ付きの型構築子と族構築子との違いは、パラメタ多相な関数と型クラスの(アドホック多相な)メソッドとの違いによく似ている。パラメタ多相な関数はあらゆる具体的な型に対して等しく振る舞うのに対し、クラスのメソッドはクラスの型パラメタに依存して振る舞いを変える。同様に、通常の型構築子はあらゆる具体的な型に対して同じデータ表現を使うことになるが、型族構築子は異なる型添字に対して異なる表現型を持つことができる。
添字付けされた型の族には三つの系統がある。データ族と開型シノニム族と閉型シノニム族である。これらは、それぞれ代数的データ型と型シノニムを添字付けされた型の族にしたものである。データの族のインスタンスとしてはデータ型とnewtypeがありえる。
型の族は-XTypeFamilies
フラグによって有効になる。GHCで型の族を使うことについてのさらなる情報が型の族についてのHaskell wikiのページにある。
データ族には次の二つの系統がある。(1)最上位で定義されるもの (2)型クラスの中に現れるもの(この場合、これは関連型と言われる)。前者の方が一般性の高い系統である。なぜなら、型添字がクラスのパラメタと一致する必要がないからである。一方、後者の方が明確に構造化されたコードにつながりやすく、なんらかの型インスタンスが(もしかすると意図せずに)省略された場合、コンパイラが警告を発する。以下では常に、まず一般的な最上位の形式について議論し、その後で関連型に課せられる追加の制約を扱う。
添字付けされたデータ族は次のようなシグネチャによって導入される。
data family GMap k :: * -> *
この特別なfamily
が、族を標準的なデータ宣言から区別する。結果の種注釈は省略可能であり、省かれた場合は通常同様に*
が使われる。例を示す。
data family Array e
名前の付いた引数にも必要なら明示的な種シグネチャを与えることができる。[http://www.haskell.org/ghc/docs/latest/html/users_guide/gadt.html GADT宣言]の場合と同様、名前付き引数は完全に省略可能なので、Array
を次のように宣言することもできる。
data family Array :: * -> *
データとnewtypeの族についてのインスタンス宣言は、通常のデータおよびnewtypeの宣言と良く似ている。違いが二つだけあって、data
あるいはnewtype
キーワードの後にinstance
が書かれることと、型引数の一部または全てが変数でない型であってもよいということである。ただし、これにforall型や型シノニム族が含まれていてはいけない。しかし、一般にデータ族が型パラメタ中にあってもよく、型シノニムも、それが完全に適用されていて、しかも展開結果が合法であるなら、認められる。これはクラスインスタンスのパラメタにおいて型シノニムが現れてよい条件と全く同じである。例として、GMap
のEither
インスタンスは次のようになる。
data instance GMap (Either a b) v = GMapEither (GMap a v) (GMap b v)
この例では、宣言にただ一つのvariantしかないが、一般にはいくつあってもよい(訳注: データ構築子が複数あってもいいということ?)。
データインスタンス宣言およびnewtypeインスタンス宣言は適切な族宣言がスコープにある場合のみ認められる。これはちょうど、クラスインスタンス宣言にクラス宣言が可視であることが必要なのと同じである。さらに、各インスタンス宣言は、その族宣言によって決まる種に準拠していなければならない。つまり、インスタンス宣言のパラメタの個数は、その族の種から決まるアリティと一致するということである。
データ族インスタンス宣言は、通常のdata
やnewtype
宣言が持つ表現力を完全に利用できる。
データ族は「data
」キーワードを使って導入されるが、族のインスタンスはdata
であってもnewtype
であってもよく、両者の混合であってもよい。例を示す。
data family T a data instance T Int = T1 Int | T2 Bool newtype instance T Char = TC Bool
data instance
はデータ構築子にGADT構文を使うことができ、また実際にGADTを定義することができる。例。
data family G a b data instance G [a] b where G1 :: c -> G [Int] b G2 :: G [a] Bool
data instance
やnewtype instance
宣言にderiving
節を使うことができる。
データ族が最上位の宣言によって定義されている場合でも、異なる族インスタンスに対して異なる計算を行う関数は相変らず型クラスのメソッドとして定義されねばならないことがある。特に、以下は不可能である。
data family T a data instance T Int = A data instance T Char = B foo :: T a -> Int foo A = 1 -- 間違い: この二つの等式をいっしょにすると... foo B = 2 -- ...型エラーが発生する
代わりに、foo
をクラスの演算として書かなければならないだろう。
class Foo a where foo :: T a -> Int instance Foo Int where foo A = 1 instance Foo Char where foo B = 2
(GADT(一般化代数的データ型)の提供する機能を見ると、上のような定義が可能であるべきだと思えるかもしれない。しかし、型族は、GADTとは対照的に、開かれている。つまり、常に新しいインスタンスを加えることが可能な(しかも別モジュールでかもしれない)のである。異なるデータインスタンスに跨がるパターン照合に対応するには、一種の拡張可能なcase構造が必要になることだろう。)
一つのプログラムの中で、あるデータ族のインスタンス宣言に少しでも重複部分があってはならない。このことは関連族であるか、そうでないかによらない。型クラスのインスタンスの場合と異なり、これは単なる整合性の問題ではなく、型安全性の問題である。
型族が出現する方法には三通りの系統がある。(1)最上位で開型族として定義されるか、(2)最上位で閉型族として定義されるか、(3)型クラスの内部に出現する(この場合、これは関連型シノニムと言われる)かである。前者の系統は、型添字がクラスパラメタと一致しなければならないという制限がないので、より一般性が高い。一方、関連型シノニムの方が明確に構造化されたコードにつながりやすく、型インスタンスが(もしかしたら意図せず)省略された場合、コンパイラが警告を発する。以下では常に、まず一般的な最上位の形式について議論し、その後で関連型に課せられる追加の制約を扱う。閉な関連型シノニムは存在しないことに注意せよ。
添字付けされた開型族は次のようなシグネチャによって導入される。
type family Elem c :: *
この特別なfamily
によって、通常の型宣言と族宣言が区別される。結果の種注釈は省略可能であり、通常と同様に、省略された場合は*
が使われる。例えば次のようになる。
type family Elem c
必要ならパラメタにも明示的な種シグネチャを付けることができる。型族宣言におけるパラメタの数をその族のアリティと呼び、その型族の適用は全てそのアリティに対して完全に飽和していなければ(訳注: つまり部分適用であっては)ならない。この制限は通常の型シノニムにはないものである。これにより、型族の種を知っただけではその族のアリティを決定できないので、型族の適用が正しいものかどうかも一般的には判断できない。例として、次の宣言を考える。
type family F a b :: * -> * -- Fの種は全体として見れば * -> * -> * -> * -- しかし、アリティは2である
この宣言のもとで、以下に正しい型と間違った型の例を示す。
F Char [Int] -- 正 種: * -> * F Char [Int] Bool -- 正 種: * F IO Bool -- 誤 最初の引数で種が不一致 F Bool -- 誤 適用が飽和していない
型族のインスタンス宣言は通常の型シノニム宣言によく似ている。違いは、type
キーワードの後にinstance
が書かれることと、型引数の一部または全部が変数でなくてもよいことの二点だけである。ただし、型引数は、forall型や型シノニム族を含んでいてはならない。一方、一般にデータ族はあってもよく、型シノニムも、完全に適用されており、かつ展開結果が合法であるなら認められる。これらはデータインスタンスの場合と全く同じ制限である。例として、[e]
のElem
インスタンスは次のようになる。
type instance Elem [e] = e
型族インスタンス宣言は、適切な族宣言がスコープにあるときのみ認められる。これはちょうど、クラスインスタンスにはクラス宣言が可視であることが必要なのと同じである。さらに、個々のインスタンス宣言は、その族宣言によって決まる種に準拠していなければならず、インスタンス宣言の型パラメタの数は族宣言の型パラメタの数と一致していなければならない。最後に、型インスタンスの右辺は単相型でなければ(つまり、forallを含んでいては)ならず、また飽和した通常の(訳注: vanilla; 型シノニム族でない)型シノニムを全て展開した時点で、族シノニム以外のシノニムが残っていてはならない。
型族はwhere
節を使って定義することもでき、その場合その型族の等式を全て定義する。例えば、
type family F a where F Int = Double F Bool = Char F a = String
閉型族の適用を単純化する際には、その型族の等式を上から順に試す。この例では、F Int
を単純化するとDouble
になり、F Bool
がChar
になり、それ以外の、Int
でもBool
でもないことが分かっている全ての型a
について、F a
がString
になるように、F
のインスタンスを宣言している。注意点として、最後の選択肢を選ぶ際には、a
がInt
ともBool
とも単一化できないことをGHCが確認できなければならない。プログラマがコード中に単にF a
と書いた場合、GHCはその型を単純化することはできないだろう。結局のところ、後でa
はInt
に具体化されるかもしれないのである。
閉型族の等式には、開型族の等式と同じ制限が適用される。
許される型インスタンスとそうでない型インスタンスの例をいくつか挙げる。
type family F a :: * type instance F [Int] = Int -- OK! type instance F String = Char -- OK! type instance F (F a) = a -- 誤: 型パラメタが型族に言及している type instance F (forall a. (a, b)) = b -- 誤: 型パラメタの中にforall型がある type instance F Float = forall a.a -- 誤: 右辺がforall型であってはならない type family H a where -- OK! H Int = Int H Bool = Bool H a = String type instance H Char = Char -- 誤: 閉型族のインスタンスは持てない type family G a b :: * -> * type instance G Int = (,) -- 誤: 引数は二つでないといけない type instance G Int Char Float = Double -- 誤: 引数は二つでないといけない
曖昧な書き換え系を定義することがないようにするために、型族の等式には、何らかの制限が課されなければならない。このため、開型族の等式は互換性を持つ(compatible)ことを要求される。二つの型パターンは、以下が成り立つなら互換性を持つ。
パターン中の対応する型が全て分離的である(apart)。または、
この二つのパターンを単一化することができ、その時に生まれる置換の下で右辺が等しい。
二つの型が分離的であると見做されるのは、どのような可能な置換に関しても、これらの型の簡約結果が同じにならないときである。
「互換性を持つ」ことの最初の条件は比較的分かり易い。これが言っているのは、二つの異なる型族インスタンスのパターンは重複してはならないということである。例えば、以下のものは許されない。
type instance F Int = Bool type instance F Int = Char
二番目の条件はもう少し興味深い。これが言っているのは、二つの型族インスタンスに重複があるとき、その重複部分において右辺が一致するなら、これらは許される、ということである。例を挙げる。
type instance F (a, Int) = [a] type instance F (Int, b) = [b] -- 重複が許される type instance G (a, Int) = [a] type instance G (Char, a) = [a] -- 非合法な重複、[Char] /= [Int]であるため
この互換性条件は、型族が関連型であるか否かにかかわらず適用されることに注意。また、これは単に整合性の問題にとどまらず、型安全性の問題であることにも注意。
「互換性」の定義には「分離性」の概念が使われるが、この概念の定義は型族の簡約に依存している。上で述べた「分離性」の定義はこのままでは検査不可能なので、以下の近似を使う。すなわち、二つの型が分離的であると見做されるのは、この二つの型を単一化することができず、さらに、無限の型を許しても単一化ができない場合である。単一化の結果が無限であることを許すので、以下の二つのインスタンスは許されない。
type instance H x x = Int type instance H [x] x = Bool
x
を無限にネストしたリストで置き換えると、この二つのインスタンスの型パターンは等しくなる。このようなインスタンスを拒絶することは型健全性のために必要である。
互換性は閉型族にも影響する。閉型族の適用を単純化する時、GHCがある選択肢を選ぶにあったっては、それより前の等式で互換性がないものがどれも適用できないことが確定していることを要求する。例を挙げる。
type family F a where F Int = Bool F a = Char type family G a where G Int = Int G a = a
F
の定義において、二つの等式には互換性がない。この二つのパターンは分離的でないし、にもかかわらずこれらの右辺は一致しない。このため、GHCは、二番目の等式を選ぶ前に、最初の等式が適用できないことを確認する必要がある。よって、F a
という型は単純化されない。Double
に単純化されるのはF Double
のような型だけである。他方、G
では、二つの等式に互換性がある。よって、GHCは、二番目の等式に注目している時には最初の等式を無視できる。このため、G a
はa
へと単純化される。
ただし、GHCiにおける重複の規則については2.4.4. 型、クラスおよびその他の宣言を見よ。
型族の存在下での型推論が決定可能であることを保証するため、型インスタンス宣言の形式に追加で制限を課す必要がある。(“Type Checking with Open Type Functions”のDefinition 5 (Relaxed Condition)も参照)。インスタンス宣言の一般形式は次の通りである。
type instance F t1 .. tn = t
ここで、t
中のすべての型族適用(G s1 .. sm)
に対して、以下のことを要求する。
s1 .. sm
に、型族構築子が一つも含まれない。
s1 .. sm
中のシンボル(データ型構築子および型変数)の数が、t1 .. tn
における数よりも厳密に小さい。
全ての型変数a
について、aがs1 .. sm
に現れる回数が、t1 .. tn
に現れる回数を超えない。
これらの制限は容易に検査することができ、型インスタンスが停止することを保証する。しかし、a ~ [F a]
のように、型構築子が族適用とデータ構築子適用の下にある、いわゆる「ループ的同値」の存在下では、型推論の完全性を保証するのに足りない。詳細については上で挙げた論文を参照のこと。
-XUndecidableInstances
オプションがコンパイラに渡されると、上記の制約は強制されず、型推論中に型族の正規化が停止することを保証するのはプログラマの責任になる。
データまたは型シノニムを型クラスの一部として宣言することができる。次にように。
class GMapKey k where data GMap k :: * -> * ... class Collects ce where type Elem ce :: * ...
このようにするとき、「family
」キーワードは使わなくても良い(使っても良い)。
型パラメタはもちろん全て型変数でなければならない。そして、そのうちのいくつか(全部でなくてよい)はクラスパラメタであってよい。個々のクラスパラメタは関連型一個につき高々一回しか使われてはならないが、使われないものがあっても良いし、クラス頭部とは違う順番であっても良い。よって、次の不自然な例は許容される。
class C a b c where type T c a x :: *
ここでc
とa
はクラスパラメタであるが、この型は第三のパラメタx
によっても添字付けされている。
関連データ族インスタンスや関連型シノニムインスタンスが型クラスインスタンスの中で宣言される場合、族インスタンスにおけるinstance
キーワードは使わなくても良い(使っても良い)。
instance (GMapKey a, GMapKey b) => GMapKey (Either a b) where data GMap (Either a b) v = GMapEither (GMap a v) (GMap b v) ... instance (Eq (Elem [e])) => Collects ([e]) where type Elem [e] = e ...
関連族インスタンスに関して最も重要な点は、クラスパラメタに対応する型添字が、インスタンス頭部で与えられる型と同一でなければならないということである。この例ではこれはGMap
の第一引数であるEither a b
であるが、これは(一つしかない)クラスパラメタと同じになっている。
関連族のインスタンスは、その族が宣言されたクラスのインスタンス宣言の一部としてしか現れることができない。これはクラスのメソッドの等式と同じである。また、これもメソッドの扱いと対応するが、クラスインスタンスにおいて関連型の宣言を省略することができる。関連族インスタンスが省略された場合、それに対応するインスタンス型は無内容(not inhabited)になる。すなわち、undefined
などの発散する式のみがその型を持てる。
珍しいことだが、単一のインスタンス宣言の中に、ある関連族のインスタンスが複数あってもよい。たとえば、次のものは合法である。
instance GMapKey Flob where data GMap Flob [v] = G1 v data GMap Flob Int = G2 Int ...
ここでは二つのデータインスタンス宣言を与えている。片方では最後のパラメタが[v]
であり、もう片方ではInt
である。これ以降に(GMap Flob ...)
のインスタンスを与えることができないので、この機能は、自由な添字変数の種が(*
とは違って)有限個の選択肢から成る場合に最も便利である。
関連型を定義するクラスは、関連型インスタンスのためにデフォルトを指定することができる。よってこれは問題ない。
class IsBoolMap v where type Key v type instance Key v = Int lookupKey :: Key v -> v -> Maybe Bool instance IsBoolMap [(Int, Bool)] where lookupKey = lookup
このinstance
キーワードは無くても良い。
重複部分がない限り、一つの型に複数のデフォルトがあっても良い。
class C a where type F a b type F a Int = Bool type F a Bool = Int
関連データ型についてはデフォルト宣言は認められない。
クラスパラメタが関連族インスタンスの右辺から可視かどうかは、そのデータ族のパラメタのみによって決まる。例として、次の単純なクラス宣言を考える。
class C a b where data T a
二つのクラスパラメタのうち一つだけがデータ族のパラメタになっている。このため、次のインスタンス宣言は不正である。
instance C [c] d where data T [c] = MkT (c, d) -- 間違ってるよ!! 「d」はスコープにない
ここでは、データインスタンスの左辺に現れない型変数d
が右辺で言及されている。このようなデータインスタンスを認めると型安全性を犠牲にすることになるので、これは認められていない。
関連型インスタンスおよび関連データインスタンス宣言は、外側のインスタンスが持つ文脈を一切受け継がない。型インスタンス宣言は、文脈を持つとしたらどういう意味なのか明確でない。データインスタンス宣言については、全ての構築子に関して文脈を繰り返すことをユーザが望んでいるとは考えにくい。この文脈が便利である可能性がある唯一の場所は関連データインスタンスに付属するderiving
節の中である。しかし、ここにおいてさえ、外側のインスタンス文脈が果たすべき役割ははっきりしていない。よって、明快さのため、我々は上の規則に従い、外側のインスタンス文脈を無視する。自動導出インスタンスに対して自明でない文脈を使う必要があるときは、独立derivingを(最上位で)使ってほしい。
エクスポートリストのための規則(Haskell Reportの5.2節)は、型族を扱えるように調整する必要がある。
T(..)
という形、ただしT
はデータ族。これはT
と、T
のデータインスタンスである構築子のうち(修飾形であれ非修飾形であれ)スコープにあるもの全てを指名する。
T(.., ci, .., fj, ..)
という形、ただしT
はデータ族。これは通常と同様に、T
と、指定された構築子ci
およびフィールドfj
を指名する。構築子とフィールドはT
のデータインスタンスのいずれかに属していなければならないが、同じものに属している必要はない。
C(..)
という形、ただしC
はクラス。これは、クラスC
と、そのメソッド全てと、その関連型を指名する。
C(.., mi, .., type Tj, ..)
という形、ただしC
はクラス。これは、クラスC
と、指定されたメソッドmi
および関連型Tj
を指名する。データ構築子と区別するために、型には「type
」キーワードが必要である。
ここまで使ってきたGMapKey
クラスの例を思い出して欲しい。
class GMapKey k where data GMap k :: * -> * insert :: GMap k v -> k -> v -> GMap k v lookup :: GMap k v -> k -> Maybe v empty :: GMap k v instance (GMapKey a, GMapKey b) => GMapKey (Either a b) where data GMap (Either a b) v = GMapEither (GMap a v) (GMap b v) ...method declarations...
以下に、エクスポートリストいくつかと、その意味を記す。
module GMap( GMapKey )
: クラス名のみをエクスポートする。
module GMap( GMapKey(..) )
: クラス、関連型GMap
、メンバ関数empty
、lookup
、insert
をエクスポートする。GMap
のデータ構築子(この場合はGMapEither
)はエクスポートされない。
module GMap( GMapKey( type GMap, empty, lookup, insert ) )
: 前項と同じ。「type
」キーワードに注意。
module GMap( GMapKey(..), GMap(..) )
: 前項と同じだが、GMap
の全ての構築子、つまりGMapEither
もエクスポートする。
module GMap ( GMapKey( empty, lookup, insert), GMap(..) )
: 前項と同じ
module GMap ( GMapKey, empty, lookup, insert, GMap(..) )
: 前項と同じ
注意点を二つ。
GMapKey(type GMap(..))
と書くことはできない。つまり、内容指定は入れ子にできない。GMap
のデータ構築子を指定するなら、それを別に書かねばならない。
次の例を考えよ
module X where data family D module Y where import X data instance D Int = D1 | D2
モジュールYは、Yで定義された全ての実体をエクスポートする。つまりデータ構築子D1
およびD2
をエクスポートするが、データ型族D
はエクスポートしない。これは(うっとおしいことに)「import Y( D(D1,D2) )
」としてYを選択的にインポートすることができないこをと意味する。YがD
をエクスポートしていないからである。こうする代わりに、エクスポートリストを明示的に指定するべきである。次のように。
module Y( D(..) ) where ... または module Y( module Y, D ) where ...
クラスインスタンスと同様に、族インスタンスは暗黙にインポートされる。ただし、これはインスタンスの頭部についてだけ適用される。インスタンスが定義するデータ構築子には適用されない。
型族のもとでは、インスタンス頭部の形式についての規則(7.6.3.2. インスタンス頭部に関する規則の緩和で与えられている)を拡張する必要がある。特に、
データ型族はインスタンス頭部に現れてもよい
型シノニム族はインスタンス頭部に(全く)現われてはいけない
後者の制約があるのは、インスタンスが適合しているか検査する方法がないからである。次の例を考えよ。
type family F a type instance F Bool = Int class C a instance C Int instance C (F a)
ここで、(C (F Bool))
という制約は両方のインスタンスに適合する。F Bool
の型インスタンスは別のモジュールにあるかもしれず、さらにはまだ書かれていないモジュールにあることさえあり得るので、この状況は特にまずい。
一方、データ族のインスタンスについての型クラスインスタンスは、通常のデータ型とほとんど同じに書ける。例えば次のように言うことができる。
data instance T Int = T1 Int | T2 Bool instance Eq (T Int) where (T1 i) == (T1 j) = i==j (T2 i) == (T2 j) = i==j _ == _ = False
クラスのインスタンスになるのは常にデータ族の特定のインスタンスであって、族全体ではないのに注意。これは要するに、一つの型族の異なる構築子に対してパターン照合を行う最上位関数を定義できないのと同じ理由による。これをするには拡張可能なcase構造が必要になるだろう。
データインスタンス宣言はまた、deriving
節を持っていても良い。例えば次のように書くことができる。
data GMap () v = GMapUnit (Maybe v) deriving Show
これは次の形のインスタンスを暗黙に定義する。
instance Show v => Show (GMap () v) where ...