yunomuのブログ

趣味のこと

record update

Haskellのrecordを使っているとこういう事をよくやる。

data Test = T { a :: Int, b :: String }

updateA :: Test -> (Int -> Int) -> Test
updateA t f = t { a = f (a t) }

Test型のデータのaをf関数を使って更新したい。

例えばほら、Stateにはmodify関数があるわけじゃないですか。

modify :: (s -> s) -> State s ()

という感じで、sを更新する関数を引数にとる。実際とはちょっと違うけど。でもレコードでもこういう感じの事がしたい。

具体的にはこう。

update name f rec = rec { name = f (name rec) }

recordのフィールドラベルまで引数として渡したいわけです。これコンパイルできないんですけどね。

さてどうしたものだろうかと思っていたところ、2010年にHaskell-cafeで同じ事を言っている人がいらっしゃいました。
[Haskell-cafe] record update
どうやらよくあるどうしようもない話題みたいです。最初の人がやりたいことと大体同じで、例としてState.modifyを挙げるところまで同じというかまさに私もsomeStateTransformみたいな事が頻出するコードを書いていてどうにかならんもんかと思って調べ始めたんですけど。
Jonathan Geddesさんの例:

someStateTransform :: State MyRecord ()
someStateTransform = do
     modify $ \record->record{field1 = (++"!") $ field1 record}
     ...

これは書きたくないよねぇ。

で、このHaskell-cafeのrecord updateスレは結構盛り上がってて、いろんな案が出てきたんですが決定打が出ないまま終わってしまいます。話自体は結構盛り上がってて、いろんなライブラリが紹介されててそれはそれで面白かったんですが。

しょうがない、やるか。

-- \f r -> r { label = f (label r) }
upd :: Name -> ExpQ
upd label = do
    f <- newName "func"
    r <- newName "rec"
    lamE
        [varP f, varP r]
        (recUpdE
            (varE r)
            [(,) label <$> [|$(varE f) ($(varE label) $(varE r))|]]
        )

これを使ってこう。

data Test = T { a :: Int, b :: String } deriving (Show)

main :: IO ()
main = do
    let test = T 1 "abc"
    print test                  -- => T {a = 1, b = "abc"}
    print ($(upd 'a) succ test) -- => T {a = 2, b = "abc"}

TemplateHaskellで作ればいいじゃないという。いいんだけどなんか、微妙に微妙。
exercises/record/Main.hs at master · yunomu/exercises · GitHub

そしてやっぱり似たような事を考える人はいるのでした。
mkEditor - Data.SemanticEditors
この場合は式じゃなくて関数定義を作ってますけど。

どがんかならんでしょうか。