割とすぐに始められるextensibleチュートリアル(レコード編)
ごきげんよう諸君。今回はextensibleについて説明しよう。
extensibleはその名の通り、拡張可能なデータ構造を提供するライブラリである。具体的には、型レベルのリストによって特徴づけられる積と和を提供する。非常に残念なことに、GHC 8.0.1ではコンパイラのバグのせいでそもそもライブラリがビルドできない*1。来たる8.0.2では修正されているので、それを待つほかない。
とにかく、ここでは積の応用技である拡張可能レコードについて紹介する。使い方は簡単だ。まず使いたいフィールド名をスペースで区切ってmkField
に渡す。
{-# LANGUAGE TemplateHaskell, DataKinds, TypeOperators, FlexibleContexts #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} import Data.Extensible import Control.Lens hiding ((:>)) mkField "name collective cry"
すると、フィールド名を表す値がTemplate Haskellによって自動生成される。仰々しい型をしているが気にせず先に進もう。
$ stack ghci --package lens --package extensible > :load tutorial.hs ... > :t name name :: forall (kind :: BOX) (f :: * -> *) (p :: * -> * -> *) (t :: (Assoc GHC.TypeLits.Symbol kind -> *) -> [Assoc GHC.TypeLits.Symbol kind] -> *) (xs :: [Assoc GHC.TypeLits.Symbol kind]) (h :: kind -> *) (v :: kind) (n :: Data.Extensible.Internal.Nat). (Labelling "name" p, Wrapper h, Data.Extensible.Internal.KnownPosition n, Extensible f p t, Elaborate "name" (FindAssoc "name" xs) ~ 'Expecting (n ':> v)) => Data.Extensible.Internal.Rig.Optic' p f (t (Field h) xs) (Repr h v)
ここで、動物の名前と、群れの呼び方、ある場合は鳴き声の擬声語を含むデータ型を定義する。
type Animal = Record [ "name" :> String , "collective" :> String , "cry" :> Maybe String ]
Record
は、フィールドの名前と型のリストをパラメータとして持つレコードの型だ。名前と型のペアは:>
で作る。
Record :: [Assoc k *] -> * (:>) :: k -> v -> Assoc k v
例としてハトとハクチョウを作ってみよう。
構築の方法はリストと同じと思ってよい。Nil
は空のレコードで、(<:)
で値を追加する。@=
でフィールド名と実際の値を指定する。
-- (@=) :: FieldName k -> v -> Field Identity (k :> v) -- infix 1 @= dove :: Animal dove = name @= "dove" <: collective @= "dule" <: cry @= Just "coo" <: Nil swan :: Animal swan = name @= "swan" <: collective @= "lamentation" <: cry @= Nothing <: Nil
作っても使えなければ意味がない、しかし心配はいらない。実は、最初に定義したフィールド名はLensとしても使えるレンズ沼仕様になっているのだ。
> swan ^. name "swan"
Lensなので当然更新もできる。地上のハクチョウはbankと呼ぶのでそれを反映させてみよう。
> swan & collective .~ "bank" name @= "swan" <: collective @= "bank" <: cry @= Nothing <: Nil
動物を取り、その集団を表す句を返す関数を定義できる。
collectiveOf :: Animal -> String collectiveOf a = unwords ["a", a ^. collective, "of", a ^. name ++ "s"]
> collectiveOf dove "a dule of doves" > collectiveOf swan "a lamentation of swans"
collectiveOf
はAnimal
型を引数として取るが、実は以下のように一般化できる。
collectiveOf :: (Associate "name" String s, Associate "collective" String s) => Record s -> String
もはや引数はAnimal
である必要はない。name
とcollective
をString
として持つなら、どんなレコードに対しても使える。これが拡張可能たる所以だ。
一見難解な仕組みに見えるが、実用するうえで必要なのはRecord
および:>
型とmkField
、@=
、<:
、Nil
だけであり、決して覚えるのが大変なものではない。同じ名前をどこでも使い回せる、lensとの相性がよいなどの性質があるので、従来のレコードの代わりに使うのも便利だ。
extensibleの拡張可能レコードは、ジェネリックな関数が書きやすいというメリットもある。以下のコードは、JSONのオブジェクトをレコードにするFromJSON
のインスタンスだ。
これは、GHCのジェネリクスを用いた実装(332行)と比べるとはるかに短い。
{-# LANGUAGE TemplateHaskell, DataKinds, TypeOperators, FlexibleContexts, UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} import Data.Extensible import Control.Lens hiding ((:>)) import Data.Aeson (FromJSON(..), withObject) import Data.Proxy import Data.String import GHC.TypeLits instance Forall (KeyValue KnownSymbol FromJSON) xs => FromJSON (Record xs) where parseJSON = withObject "Object" $ \v -> hgenerateFor (Proxy :: Proxy (KeyValue KnownSymbol FromJSON)) $ \m -> let k = symbolVal (proxyAssocKey m) in case v ^? ix (fromString k) of Just a -> Field . pure <$> parseJSON a Nothing -> fail $ "Missing key: " ++ k
Forall
とhgenerateFor
がこのインスタンスの要になっている。Forall c xs
は、「xsのすべての要素が制約cを満たす」という制約で、この場合、すべてのフィールドについて、名前は型レベル文字列、値の型はFromJSON
のインスタンスであることを示している。hgenerateFor
はその制約を用いてレコードを一気に構築する関数だ。
Forall :: (k -> Constraint) -> [k] -> Constraint hgenerateFor :: (Applicative f, Forall c xs) => proxy c -> (forall (x :: k). c x => Membership xs x -> f (h x)) -> f (h :* xs)
Membership xs x
はx
がxs
の要素であることを表す型である。ここでは、フィールド名をsymbolVal
とproxyAssocKey
でString
に変換するために使われている。Field . pure
は具体的なフィールド名を指定せずにフィールドを構築する関数である。
> decode <$> fromString <$> getLine :: IO (Maybe Animal) {"name": "zebra", "collective": "dazzle", "cry": "whoop"} Just (name @= "zebra" <: collective @= "dazzle" <: cry @= Just "whoop" <: Nil)
リポジトリにはToJSONのインスタンスの例もある。
レコードに関しては、GHCのGenericsとは比べ物にならないほど簡潔に汎用的なインスタンスを記述できる。もし自前のクラスについてジェネリックなインスタンスを定義したいが、複雑で実装が難しいという場合は、ぜひextensibleを使ってみてほしい。
ここで紹介したのはextensibleの一部に過ぎない。次回は、レコードと対をなす構造、拡張可能ヴァリアントについて紹介したい。