7.24. 総称プログラミング

-XDeriveGeneric(7.5.3. より広範なクラスについてのderiving節(TypeableDataなど))と-XDefaultSignatures (7.6.1.4. デフォルトメソッドシグネチャ)を組み合わせることで、データ型について総称的なプログラミングをGHC.Genericsを使って簡単に行なえる。この節は、その方法について極めて短かい概観を与える。

GHCにおける総称プログラミングでは、ユーザがインスタンスを作成する際にメソッドを指定する必要がないようなクラスを定義することが可能になる。メソッド本体はGHCによって自動的に導出される。これは例えばReadShowなどの標準クラスの事情に似ているが、ユーザ定義のクラスについでである。

7.24.1. 表現を導出する

まず必要なのは総称的な型表現である。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クラスおよびGeneric1クラスは、ユーザ定義のデータ型と、それの積和形(sum-of-products)の内部表現との間の仲介を行なう。

class Generic a where
  -- ユーザのデータ型の表現をエンコードする
  type Rep a :: * -> *
  -- データ型をその表現に変換する
  from  :: a -> (Rep a) x
  -- 表現をデータ型に変換する
  to    :: (Rep a) x -> a

class Generic1 f where
  type Rep1 f :: * -> *

  from1  :: f a -> Rep1 f a
  to1    :: Rep1 f a -> f a

Generic1は、 mapのように型コンテナについてのみ定義できる関数に使われる。総称的なインスタンスを自動的に定義するにはこれらのクラスのインスタンスが必要であり、これは-XDeriveGeneric (7.5.3. より広範なクラスについてのderiving節(TypeableDataなど))を付けると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 R 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も使える。

7.24.2. 総称関数を書く

総称関数は、クラスを作り、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

このクラスは表現型以外に関するインスタンスを持っても意味がないので、典型的にはエクスポートされない。

7.24.3. 総称的デフォルト

あとは、ユーザに露出される「フロントエンド」クラスを定義するだけである。

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のデフォルトメソッド(これは直列化の総称実装に対応する)が使われる。もっと多くの総称関数の例がHackageのgeneric-derivingパッケージにあるので参照して欲しい。

7.24.4. さらなる情報

さらなる詳細については、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.