-XDeriveGeneric
(7.5.3. より広範なクラスについてのderiving節(Typeable
、Data
など))と-XDefaultSignatures
(7.6.1.4. デフォルトメソッドシグネチャ)を組み合わせることで、データ型について総称的なプログラミングをGHC.Generics
使って簡単に行なえる。この節は、その方法について極めて短かい概観を与える。
GHCにおける総称プログラミングでは、ユーザがインスタンスを作成する際にメソッドを指定する必要がないようなクラスを定義することが可能になる。メソッド本体はGHCによって自動的に導出される。これは例えばRead
やShow
などの標準クラスの事情に似ているが、ユーザ定義のクラスについでである。
まず必要なのは総称的な型表現である。GHC.Generics
には数個の基本的な型が定義されていて、これらを使ってHaskellのデータ型を表現することができる。
-- | 単位: 引数のない構築子に使われる data U1 p = U1 -- | 定数、追加のパラメタ、種*の再帰 newtype K1 i c p = K1 { unK1 :: c } -- | メタ情報 (構築子名など) newtype M1 i c f p = M1 { unM1 :: f p } -- | 和: 構築子間での選択をエンコードする infixr 5 :+: data (:+:) f g p = L1 (f p) | R1 (g p) -- | 積: 構築子への複数の引数をエンコードする infixr 6 :*: data (:*:) f g p = f p :*: g p
Generic
クラスは、ユーザ定義のデータ型と、それの積和形(sum-of-products)の内部表現との間の仲介を行なう。
class Generic a where -- ユーザのデータ型の表現をエンコードする type Rep a :: * -> * -- データ型をその表現に変換する from :: a -> (Rep a) x -- 表現をデータ型に変換する to :: (Rep a) x -> a
総称的なインスタンスを自動的に定義するにはこのクラスのインスタンスが必要であり、これは-XDeriveGeneric
(7.5.3. より広範なクラスについてのderiving節(Typeable
、Data
など))を付けるとGHCによって自動導出できる。
例えば、木のユーザ定義データ型data UserTree a = Node a (UserTree a) (UserTree a) | Leaf
は以下の表現を得る。
instance Generic (UserTree a) where -- 表現型 type Rep (UserTree a) = M1 D D1UserTree ( M1 C C1_0UserTree ( M1 S NoSelector (K1 P a) :*: M1 S NoSelector (K1 R (UserTree a)) :*: M1 S NoSelector (K1 R (UserTree a))) :+: M1 C C1_1UserTree U1) -- 変換関数 from (Node x l r) = M1 (L1 (M1 (M1 (K1 x) :*: M1 (K1 l) :*: M1 (K1 r)))) from Leaf = M1 (R1 (M1 U1)) to (M1 (L1 (M1 (M1 (K1 x) :*: M1 (K1 l) :*: M1 (K1 r))))) = Node x l r to (M1 (R1 (M1 U1))) = Leaf -- メタ情報 data D1UserTree data C1_0UserTree data C1_1UserTree instance Datatype D1UserTree where datatypeName _ = "UserTree" moduleName _ = "Main" instance Constructor C1_0UserTree where conName _ = "Node" instance Constructor C1_1UserTree where conName _ = "Leaf"
この表現は、deriving Generic
がデータ型に付属していれば自動的に生成される。独立derivingも使える。
総称関数は、クラスを作り、GHC.Generics
の各表現型についてインスタンスを与えることで定義できる。例として総称的な直列化を示す。
data Bin = O | I class GSerialize f where gput :: f a -> [Bin] instance GSerialize U1 where gput U1 = [] instance (GSerialize a, GSerialize b) => GSerialize (a :*: b) where gput (x :*: y) = gput x ++ gput y instance (GSerialize a, GSerialize b) => GSerialize (a :+: b) where gput (L1 x) = O : gput x gput (R1 x) = I : gput x instance (GSerialize a) => GSerialize (M1 i c a) where gput (M1 x) = gput x instance (Serialize a) => GSerialize (K1 i a) where gput (K1 x) = put x
このクラスは表現型以外に関するインスタンスを持っても意味がないので、典型的にはエクスポートされない。
あとは、ユーザに露出される「フロントエンド」クラスを定義するだけである。
class Serialize a where put :: a -> [Bin] default put :: (Generic a, GSerialize (Rep a)) => a -> [Bit] put = gput . from
ここでは、デフォルトシグネチャを使って、インスタンスとなる型にGeneric
インスタンスがある限り、ユーザがput
の実装を提供する必要がないということを指定している。例えば、UserTree
型に関しては、ユーザは単に次のように書くことができる。
instance (Serialize a) => Serialize (UserTree a)
するとput
のデフォルトメソッド(これは直列化の総称実装に対応する)が使われる。
さらなる詳細については、HaskellWikiのページか以下の元論文を参照せよ。
Jose Pedro Magalhaes, Atze Dijkstra, Johan Jeuring, and Andres Loeh. A generic deriving mechanism for Haskell. Proceedings of the third ACM Haskell symposium on Haskell (Haskell'2010), pp. 37-48, ACM, 2010.
Generic
のインスタンスを自動導出することしか許されていない。Generic1
の自動導出への対応(よって、fmap
などの種* -> *
の総称関数を可能にすること)はもっと後の段階になる。