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