Generics
その名前の通り、
ジェネリックプログラミングを実現する技術。具体的には、「任意の
データ型」と「どんなデータ型でも表現できる型」を相互変換する仕組みのおかげで、後者に対する関数を一度定義しておけば、前者に対する関数をいちいち作る必要がなくなるというものだ。
「データ型を書くだけで、その操作の最適な実装をバグを出さずに導出する」ことを目指すものであり、
Haskellの競争力の一つとなる重要な概念である。使いすぎると
コンパイル時間の増大を招く点には注意(それでも、個別の実装やデバッグの手間を圧倒的に減らせるが)。
GHC.Generics
GHC.Generics
というモジュールが標準ライブラリで提供されている。 Generic
が、任意の型と共通の型の橋渡しとなるクラスである。
haskellclass 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
ドキュメントの例を拝借すると、共通の部品の型はこのような見た目をしている。
haskellinstance 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)
型クラスを活用すれば、この共通の型に対する操作は容易に記述できる。 試しにデータ型のフィールドを全部文字列に変換してリストにする関数を作ってみよう。
haskellclass 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行も書く必要はなくなる――どんなデータ型であっても。
haskellinstance 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つに分割する。これがジェネリクスの基本形の第二形態だ。
haskellinstance 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によって型レベルでメタデータを表現するためのものだ。
haskelldata Meta = MetaData Symbol Symbol Symbol Bool
| MetaCons Symbol FixityI Bool
| MetaSel (Maybe Symbol) SourceUnpackedness SourceStrictness DecidedStrictness
活用例
関連項目