レコード問題
標準のレコードが持つ問題点
1. 名前問題が解決できていない。つまり同じモジュールの中で同じフィールド名をもつ2つのレコードを定義できない。例えば以下はコンパイルできない。
code:hs
data A = A { field :: String }
data B = B { field :: String }
2. 部分的である。以下のコードはコンパイル時に何も警告しないのにランタイム時にエラーになる。
code:hs
data A = A1 { field1 :: String } |
A2 { field2 :: String }
main = print $ field1 $ A2 "abc"
3. 型コンストラクタ間で違う型を持つ同じ名前のフィールドを定義することが出来ない。
code:hs
data A = A1 { field :: String } |
A2 { field :: Int }
4. ネストしたレコードの値を更新することは指数関数的に面倒くさくなる
code:hs
addManStk team = team {
manager = (manager team) {
diet = (diet (manager team)) {
steaks = steaks (diet (manager team)) + 1
}
}
}
OverloadedRecordFields
GHCの言語拡張でレコードの問題を解決しようという試み
すべての機能が入るまでの道のりは3ステップに分けられる
Step1. DuplicateRecordFields
同じモジュール内で同じフィールド名をもつレコードを定義できるようにする言語拡張
code:hs
data Person = MkPerson { personId :: Int, name :: String }
data Address = MkAddress { personId :: Int, address :: String }
GHC 8.0 で既に実装されている
Step2. OverloadedLabels
fromLabel @"field" @alpha proxy# の糖衣構文として #field のような書き方ができるようになる。 fromLabel は以下の型クラス IsLabel のメソッド。
code:hs
class IsLabel (x :: Symbol) a where
fromLabel :: Proxy# x -> a
具体的に IsLabel のインスタンスが定義されるわけではないので OverloadedLabels だけでは使いみちは少ないが将来的にレコードが宣言されると適切なインスタンスが自動的に生成されることを予定している。
GHC 8.0 で既に実装されている
Step3. Magic type classes
GHCがコンパイル時にレコードの宣言を見つけると以下のような型クラスのインスタンスを作るようになる。
code:hs
-- | HasField x r a はrが型aのフィールドxを持つレコードであることを意味している
class HasField (x :: Symbol) r a | x r -> a where
-- | レコードからフィールドを取り出す
getField :: Proxy# x -> r -> a
-- | UpdateField x s t bはsが型tのレコードに型bの値をセットできる
-- フィールドxを持つレコードの型であることを意味している
class UpdateField (x :: Symbol) s t b | x t -> b, x s b -> t where
-- | フィールドをレコードにセットする
setField :: Proxy# x -> s -> b -> t
例えば
code:hs
data T = MkT { x :: Int }
のようなレコードが定義されると
code:hs
instance HasField "x" T Int where
getField _ = x
instance UpdateField "x" T T Int where
setField _ (MkT _) x = MkT x
のようなインスタンスが宣言される。
IsLabel とは以下のようなインスタンスによって結びついている。
code:hs
instance (HasField x r a) => IsLabel x (r -> a) where
fromLabel = getField (proxy# :: Proxy# x)