Generics
その名前の通り、ジェネリックプログラミングを実現する技術。具体的には、「任意のデータ型」と「どんなデータ型でも表現できる型」を相互変換する仕組みのおかげで、後者に対する関数を一度定義しておけば、前者に対する関数をいちいち作る必要がなくなるというものだ。「データ型を書くだけで、その操作の最適な実装をバグを出さずに導出する」ことを目指すものであり、Haskellの競争力の一つとなる重要な概念である。使いすぎるとコンパイル時間の増大を招く点には注意(それでも、個別の実装やデバッグの手間を圧倒的に減らせるが)。 GHC.Generics
GHC.Genericsというモジュールが標準ライブラリで提供されている。Generic が、任意の型と共通の型の橋渡しとなるクラスである。
code:haskell
class Generic a where
type Rep a :: Type -> Type
from :: a -> Rep a x -- 共通の部品に変換する
to :: Rep a x -> a -- 共通の部品から変換する
このインスタンスは、DeriveGeneric拡張によってGHCが導出してくれるのがミソだ。
code:haskell
{-# LANGUAGE DeriveGeneric, TypeOperators, DefaultSignatures, FlexibleContexts #-} import GHC.Generics
data Tree a = Leaf a | Node (Tree a) (Tree a)
deriving Generic
ドキュメントの例を拝借すると、共通の部品の型はこのような見た目をしている。
code: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がフィールド、:+: が和、:*: が積を表していると理解しやすい。
code:haskell
instance Generic (Tree a) where
type Rep (Tree a) = K1 R a :+: K1 R (Tree a) :*: K1 R (Tree a)
型クラスを活用すれば、この共通の型に対する操作は容易に記述できる。 試しにデータ型のフィールドを全部文字列に変換してリストにする関数を作ってみよう。 code:haskell
class GShowAll f where
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
code:haskell
*Main> gshowAll $ from (Node (Leaf 1) (Leaf 2))
振る舞いこそ再帰的ではないが、期待した挙動が得られている。
型クラスのデフォルト実装を導出するのがジェネリクスの最もポピュラーな活用法だ。GShowAllを一般化した、showAll :: a -> [String]を持つ型クラスを考えてみよう。このデフォルト実装は、gshowAll . fromによって与えられる。
code:haskell
class ShowAll a where
default showAll :: (Generic a, GShowAll (Rep a)) => a -> String showAll = gshowAll . from
instance ShowAll Int where
showAll = pure . show
GShowAllのインスタンスを以下のように書き換えれば、GShowAllとShowAllを再帰的に連携させることができる。
code: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行も書く必要はなくなる――どんなデータ型であっても。
code: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つに分割する。これがジェネリクスの基本形の第二形態だ。
code: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によって型レベルでメタデータを表現するためのものだ。
code:haskell
data Meta = MetaData Symbol Symbol Symbol Bool
| MetaCons Symbol FixityI Bool
| MetaSel (Maybe Symbol) SourceUnpackedness SourceStrictness DecidedStrictness
活用例
関連項目