generated at
Generics
その名前の通り、ジェネリックプログラミングを実現する技術。具体的には、「任意のデータ型」と「どんなデータ型でも表現できる型」を相互変換する仕組みのおかげで、後者に対する関数を一度定義しておけば、前者に対する関数をいちいち作る必要がなくなるというものだ。「データ型を書くだけで、その操作の最適な実装をバグを出さずに導出する」ことを目指すものであり、Haskellの競争力の一つとなる重要な概念である。使いすぎるとコンパイル時間の増大を招く点には注意(それでも、個別の実装やデバッグの手間を圧倒的に減らせるが)。

GHC.Generics

GHC.Generics というモジュールが標準ライブラリで提供されている。 Generic が、任意の型と共通の型の橋渡しとなるクラスである。
haskell
class Generic a where type Rep a :: Type -> Type from :: a -> Rep a x -- 共通の部品に変換する to :: Rep a x -> a -- 共通の部品から変換する
このインスタンスは、DeriveGeneric拡張によってGHCが導出してくれるのがミソだ。
haskell
{-# LANGUAGE DeriveGeneric, TypeOperators, DefaultSignatures, FlexibleContexts #-} import GHC.Generics data Tree a = Leaf a | Node (Tree a) (Tree a) deriving Generic

ドキュメントの例を拝借すると、共通の部品の型はこのような見た目をしている。
haskell
instance Generic (Tree a) where type Rep (Tree a) = M1 D ('MetaData "Tree" "Main" "package-name" 'False) (M1 C ('MetaCons "Leaf" 'PrefixI 'False) (M1 S ('MetaSel 'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (K1 R a)) :+: M1 C ('MetaCons "Node" 'PrefixI 'False) (M1 S ('MetaSel 'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (K1 R (Tree a)) :*: M1 S ('MetaSel 'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (K1 R (Tree a))))

大変ごちゃごちゃしていて見づらいが、メタデータを表す M1 を取り除くと、 K1 R がフィールド、 :+: が和、 :*: が積を表していると理解しやすい。
haskell
instance Generic (Tree a) where type Rep (Tree a) = K1 R a :+: K1 R (Tree a) :*: K1 R (Tree a)

型クラスを活用すれば、この共通の型に対する操作は容易に記述できる。 試しにデータ型のフィールドを全部文字列に変換してリストにする関数を作ってみよう。

haskell
class GShowAll f where gshowAll :: f x -> [String] instance GShowAll U1 where gshowAll _ = [] -- data Proxy x = Proxy のような、フィールドのない型 instance Show a => GShowAll (K1 i a) where gshowAll (K1 a) = [show a] -- 各フィールド instance (GShowAll f, GShowAll g) => GShowAll (f :*: g) where gshowAll (f :*: g) = gshowAll f ++ gshowAll g instance (GShowAll f, GShowAll g) => GShowAll (f :+: g) where gshowAll (L1 f) = gshowAll f gshowAll (R1 g) = gshowAll g instance GShowAll f => GShowAll (M1 t m f) where -- メタデータは無視する gshowAll (M1 f) = gshowAll f

haskell
*Main> gshowAll $ from (Node (Leaf 1) (Leaf 2)) ["Leaf 1","Leaf 2"]

振る舞いこそ再帰的ではないが、期待した挙動が得られている。

型クラスのデフォルト実装を導出するのがジェネリクスの最もポピュラーな活用法だ。 GShowAll を一般化した、 showAll :: a -> [String] を持つ型クラスを考えてみよう。このデフォルト実装は、 gshowAll . from によって与えられる。

haskell
class ShowAll a where showAll :: a -> [String] default showAll :: (Generic a, GShowAll (Rep a)) => a -> [String] showAll = gshowAll . from instance ShowAll Int where showAll = pure . show

GShowAllのインスタンスを以下のように書き換えれば、 GShowAll ShowAll を再帰的に連携させることができる。
diff
- instance Show a => GShowAll (K1 i a) where gshowAll (K1 a) = [show a] -- 各フィールド + instance ShowAll a => GShowAll (K1 i a) where gshowAll (K1 a) = showAll a -- 各フィールド

一度この仕組みを確立させてしまえば、 ShowAll のインスタンスを定義するのにコードを1行も書く必要はなくなる――どんなデータ型であっても。

haskell
instance ShowAll a => ShowAll (Tree a) data Location = Location { x :: Double, y :: Double } deriving (Show, Generic) instance ShowAll Location

U1 , K1 , :*: , :|: , M1 のインスタンス定義と、 DefaultSignature による基本形を覚えておけば、シリアライズやテストなど様々な問題に応用できる(テストに出るぞ!)。

応用編: メタデータの取得

M1型からは、データ型の名前、コンストラクタ、フィールド名や、正格性などの情報を取り出せる。まず、 M1 に対して一括で定義していたインスタンスを3つに分割する。これがジェネリクスの基本形の第二形態だ。
haskell
instance GShowAll f => GShowAll (D1 m f) where -- データ型 instance GShowAll f => GShowAll (C1 m f) where -- コンストラクタ instance GShowAll f => GShowAll (S1 m f) where -- フィールド

このMeta種は、DataKindsによって型レベルでメタデータを表現するためのものだ。

haskell
data Meta = MetaData Symbol Symbol Symbol Bool | MetaCons Symbol FixityI Bool | MetaSel (Maybe Symbol) SourceUnpackedness SourceStrictness DecidedStrictness

活用例

関連項目

Scrap Your Boilerplate - 完全に過去の遺物。