extensibleについて
H勉強会で次回やるみたいな話をしたのでそれに向けた適当なメモ書き
拡張可能レコード
extensibleパッケージで提供されている,フィールドが拡張可能なレコード.名前そのまんま
既存のレコードの問題点
ファックなことにフィールド名はレコードセレクタ関数になるので平然と値の名前空間を侵略してくる
部分的である.
code: extensible1.hs
data A = A1 { field1 :: String } |
A2 { field2 :: String }
main = print $ field1 $ A2 "abc" -- コンパイルエラーにならず,実行時エラーになる.
拡張できない(それはそう)
フィールドがファーストクラスではない
フィールド名自体はデータではなく制約として扱われる(OverLoadedLabelsの場合)
つまりはフィールド多相な関数が使えない
ネストしまくったレコードのフィールドにアクセスするのがめんどい
extensible
拡張可能なレコード(直積),拡張可能ヴァリアント(直和)を提供とそれにまつわる型クラス,関数を提供する
フィールド名を型レベルに昇格して,その型とフィールドに格納する値を紐付け,フィールド名自体は型レベルリストに格納して拡張可能なレコードを実現する.(本質的には(型レベルの)リストなのでフィールドを追加できる)
イメージとしては連想配列(なおKeyは型)
拡張可能ヴァリアントも似たような実装になっている
使い方
定義はこんな感じ
code: extensible2.hs
-- 普通のレコード
data User =
{ id :: ID
, name :: Text
, age :: Int
}
-- 拡張可能レコード
type User = Record
'[ "id" >: ID
, "name" >: Text
, "age" >: Int
]
-- ヴァリアント
data Color = RGB Int Int Int | CMYK Int Int Int Int
-- 拡張可能ヴァリアント
type Color = Variant
'[ "rgb" >: (Int,Int,Int)
, "cmyk" >: (Int,Int,Int,Int)
]
上記のフィールド名は型,フィールド名への注釈もまた型である.このような記法で定義するためにDataKind拡張を,>:という型レベル演算子を使うためにTypeOperators拡張を使用している.>:はAssoc key val型を作る型コンストラクタであり,Assoc key valは値レベルでいうところの(key, val)に相当する.
実際にこの拡張可能レコードを使うには以下のようにする
code: extensible3.hs
user1 :: User
user1
<: nil
-- lensの(^.),(.~)をつかって以下のうようにフィールドにアクセスできる
#フィールド名 @= 値という感じでフィールドを定義,<:で繋いでいく.(型レベル)リストとしてみれば<:はConsに対応する
#フィールド名という形でフィールドを指定できるのはOverloadedLavels拡張による.
型クラス⊆であるレコードxsがysのレコードを(順番に関係なく)すべて持っていることも表現できる.
これを用いてshrink関数で一部のフィールドの切り出しや順番の入れ替えができる.
happendを使えば拡張もできる.
拡張可能ヴァリアントの場合以下のようになる
code: extensible4.hs
-- Lensの(#)を使用している.embedAssocはFieldからVariantを作ることができる関数である.
color1 = #rgb # (0,0,0) :: Color color2 = embedAssoc $ #cmyk @= (0,0,0,0) :: Color spread関数でヴァリアントの一部を合成してヴァリアントを作ることができる.
型クラスのインスタンスにする
extensibleのパッケージに含まれるForAllを使い型クラス制約を与えることでインスタンスにすることができる.
code: extensible5.hs
-- xsが拡張可能ヴァリアント Hoge型クラスのインスタンスにする
instance (ForAll KeyValue KnownSymbol Hoge) xs => Hoge (Variant xs) where
...
ForAll KeyValue KnownSymbol Hogeはすべての型レベルリストの要素(k >: v)が 制約KnownSymbol k及びHoge vを満たすということである.ForAllはこのような意味をもつ制約を計算する型レベル関数になっている.
フィールド多相の関数
code: extensible6.hs
type Person = Record
'[ "personId" :> Int
, "name" :> String
]
type Address = Record
'[ "personId" :> Int
, "address" :> String
]
-- personIdのフィールドを持つレコードに共通の処理を提供(フィールド多相)
getPersonId :: Associate "personId" Int xs => Record xs -> Int
再帰的な定義
再帰的な定義は型シノニムでは行えないので,newtypeをつかうこと.
既存のレコードの問題点の解決
extensibleの拡張可能レコードは既存のレコードの問題点をほとんど解決している.
フィールド名の名前空間への侵食 -> 型レベルにすることで衝突しなくなる
部分的である -> 型検査で上記のような場合を弾くことができる.
拡張できない -> 型レベルリストなので拡張できる
フィールドがファーストクラスでない -> フィールドを型にすることで第一級に扱える
ネストしたレコードへのアクセス -> フィールドに対してlensのアクセス方法を流用できることで簡単にアクセスできる