Haskellのレコード更新構文を詳しく
スタック・オーバーフローにて
haskell - Text.Parser.Token.StyleのemptyIdentsの使い方について - スタック・オーバーフロー
といった質問をしたところ、このような回答を頂き、レコード更新構文についての詳細を知る必要を感じたので私的まとめ。
といってもほとんどHaskell Language Reportを見ていろいろ実験しただけなんですが。
レコード更新構文の解釈
3.15.3 Updates Using Field Labelsより。
をコンストラクタ、を束縛の列、を値として、を次のように定義します:
「コンストラクタの番目のフィールドの名前がであり、かつ束縛の列にに関する束縛が含まれるならばである。そうでないならばである。」
データ型Tを
data T = { :: ... :: }
| ...
| { :: ... :: }
とすると、T型の式に対するレコード更新構文 {}は
{} = case of
... -> ...
...
... -> ...
に変換されます。
多相的な値と組合せるときの注意点
変換された式を見ると、要するにただのパターンマッチになっていることがわかります。
マッチされる式 e の型が多相的で、かつユーザが型シグネチャを供給していないならば e の型は精密化されません。
となると、例えば e が多相的な型を持ち、かつ e {bs} に単相的な型をユーザが与えていた場合、 e の値によって e {bs} の値が一意に決まらなかった場合などは困るわけで、このような場合曖昧性エラーが生じます。
具体的には以下のようなコードで曖昧性エラーが発生します。
data T a = T {element :: a, typeStr :: String} class F a where mes :: a -> String instance F Int where mes _ = "Int" instance F Double where mes _ = "Double" mkDefaultT :: F a => a -> T a mkDefaultT x = T x (mes x) defaultT :: (F a, Num a) => T a defaultT = mkDefaultT 0 -- error! modifiedT :: T Double modifiedT = defaultT {element = 0.0}
defaultT は T Int 型のとき typeStr = "Int" で、 T Double 型のとき typeStr = "Double" です。なので、上の modifiedT の定義では defaultT {element = 0.0} の typeStr フィールドが "Int" なのか "Double" なのかが決定できす、曖昧性エラーとなります。
GHCはどこまで賢くやってくれるのか、という問題
上のような事情があるにはあるのですが、例えば以下のようなコードはコンパイルを通ります。
data T a = T {element :: String, typeStr :: String} defaultT :: T a defaultT = T "" "a" -- error! modifiedT :: T Double modifiedT = defaultT {element = ""}
このケースでは defaultT にはいかなる制約もかかっていない forall a . T a という型がついており、このような場合は a がどんな型に具体化されようと e の値は変わらず T "" "a" です。
なので、 defaultT の型が曖昧でも modifiedT の定義は上のままでうまくいきます。
では、「型変数がどの型に具体化されても値が同じ」場合につねにうまくいくかというとそういうわけにもいかず、以下のコードはエラーになります。
data T a = T {element :: String, typeStr :: String} class F a where mes :: a -> String instance F Int where mes _ = "Int" instance F Double where mes _ = "Double" defaultT :: F a => T a defaultT = T "" "a" -- error! modifiedT :: T Double modifiedT = defaultT {element = ""}
単に defaultT の型に制約を増やしただけですが、これはエラーになります。
これはおそらく「型変数がどの型に具体化されても値が同じ」かどうかは型によってのみ判断されるからです。
forall a . F a => T a の型の値は「型変数 a が何に具体化されても同じ値」とは限らず、例えば
polyT :: F a => T a polyT = mkPoly undefined where mkPoly :: F a => a -> T a mkPoly x = T "" (mes x)
のような値がありえます。
同じフィールド名に対するレコード更新構文
知ってた人には「今更」って感じなんでしょうが、
data User = Registered {uid :: Int, name :: String} | Guest {name :: String} deriving (Show) updateName :: User -> String -> User updateName u n = u {name = n} -- 動作: -- *Main> updateName (Registered {uid=0, name="Bob"}) "Alice" -- Registered {uid = 0, name = "Alice"} -- *Main> updateName (Guest {name="Alice"}) "Bob" -- Guest {name = "Bob"}
みたいなの書けたんですね。知りませんでした。*1
*1:というかそもそも同じデータ型の定義中でならフィールド名被っててもいいのだということを初めて知りました。