HDBCとReaderTモナドを組み合わせてみる

HDBCでDBを操作するプログラムを書いていると、少なからずDB接続(IConnection)がうっとうしくなってきます。DB操作を行う関数は全て、引数にDB接続を取るからです。

例えばCRUD操作を行う関数を書いたとします。

insertC c = runRaw c "insert into COUNTER values (0)"
selectC c = quickQuery c "select * from COUNTER order by COUNT" []
updateC c = runRaw c "update COUNTER set count = count + 1"
deleteC c = runRaw c "delete from COUNTER"

これらの関数を使うときは>>=で連結して使うわけですが、DB接続を全部の関数に渡して回る必要があるわけです。

procC :: IO [[SqlValue]]
procC = connectPostgreSQL connectionString
  >>= \c -> return c
  >>  insertC c
  >>  updateC c
  >>  commit c
  >>  selectC c

関数型言語は状態を持たないので、引数でDB接続を受け取るしかない。わかってる。それはわかってるんですがやっぱりうっとーしい。

そこでReaderモナドを使ってDB接続を渡さなくてすむようにしてみようと思ったのですが、かなりはまりました。ReaderモナドとIOモナドの組み合わせではうまく動作しなくて、ReaderTモナドを使わないといけない、と気付くのに時間がかかりました。

ReaderTを使ったCRUD操作は以下のようになりました。

insertT :: IConnection c => ReaderT c IO Integer
insertT = ask >>= \c -> liftIO $
  run c "insert into COUNTER values (0)" []

selectT :: IConnection c => ReaderT c IO [[SqlValue]]
selectT = ask >>= \c -> liftIO $
  quickQuery c "select * from COUNTER order by COUNT" []

updateT :: IConnection c => ReaderT c IO Integer
updateT = ask >>= \c -> liftIO $
  run c "update COUNTER set count = count + 1" []

deleteT :: IConnection c => ReaderT c IO Integer
deleteT = ask >>= \c -> liftIO $
  run c "delete from COUNTER" []

commitT :: IConnection c => ReaderT c IO ()
commitT = ask >>= \c -> liftIO $ commit c

簡潔とは言えないですが、パターンはありますね。

ask >>= \c -> liftIO $

の後に続けてDB操作を記述すればいいです。
あと型の宣言は必須。これがないと型が曖昧だとエラーを吐かれます。

さてReaderTモナドCRUD関数を使ってみます。

proc :: IO [[SqlValue]]
proc = connectPostgreSQL connectionString
  >>= \c -> runReaderT (
           return ()
        >> insertT
        >> updateT
        >> commitT
        >> selectT
      ) c

確かに引数を渡しまくる必要はなくなったけど、可読性が犠牲になった感じ。こんなときは関数分割。

proc2 :: IO [[SqlValue]]
proc2 = connectPostgreSQL connectionString
  >>= runReaderT procT
procT = return ()
  >> insertT
  >> updateT
  >> commitT
  >> selectT

これでどうかな?