Servant と Relational Record でウェブアプリケーション開発

Servant とは

Servant は型レベルプログラミングによって、ウェブアプリとしてのインターフェースと実装との差異を防ぐことのできるウェブアプリフレームワークです。

haskell-servant.readthedocs.io

日本語記事としては lotz さんのこちらが分かりやすいので、参考にしてください。

qiita.com

Haskell Relational Record とは

Haskell Relational Record は言語内 DSL によって SQL を生成するもので、正しくない SQL に相当するものは型エラーとなります。

khibino.github.io

この2つを組み合わせることで、ユーザーからのリクエストから DB 操作を経てレスポンスの返答まで型に守られて開発ができるようになります。

この記事で解説するソースコードこのリポジトリーで公開しています。

github.com

Stack の resolver は 12.26 を使用しています。

作るもの

下記のようなインタフェースを持ったアプリを作ります。

  • /
    • HTML が返る
  • /books
    • 書籍情報のマップが JSON で返る
  • /book/<id>
    • id で指定された書籍情報が JSON で返る

/books で返る JSON は次のような形式です。

{
  "ABC of Read": {
    "name": "ABC of Read",
    "auther": "Mary"
  },
  "The Good Text": {
    "name": "The Good Text",
    "auther": "John"
  }
}

/book/1 で返る JSON は次のような形式です。

{
  "name": "The Good Text",
  "auther": "John"
}

ハンドラーの型を拡張する

DB 接続を扱うということは、ハンドラー内で接続にアクセスできないといけません。ここでは Reader モナドトランスフォーマーを Servant のハンドラーに積みます。ロガーも使うのでその上に Logging モナドトランスフォーマーも積みました。

type Handler = LoggingT (ReaderT (Pool Connection) Servant.Handler)

Api 型とそれぞれのハンドラーが定義されている場合、ハンドラー拡張前では Application 型の値は次のようになっているでしょう。この部分は積んだモナドトランスフォーマーをはがすように書きかえなければいけません。

app :: Application
app = serve api server

api :: Proxy Api
api = Proxy

server :: Pool Connection -> Server Api
server pool =
  Index.handler :<|>
  Books.handler :<|>
  Book.handler

次のように書きかえます。

makeApp :: IO Application
makeApp = do
  pool <-
    createPool
    connect
    disconnect
    1 -- stripes
    1 -- time for keeping open
    5 -- resource per stripe
  pure $ serve api $ server pool

api :: Proxy Api
api = Proxy

server :: Pool Connection -> Server Api
server pool =
  hoistServer
    api
    (flip runReaderT pool . runStdoutLoggingT)
    $ Index.handler :<|>
      Books.handler :<|>
      Book.handler

接続は再利用したいので resource-pool を用います。server 関数は hoistServer 関数を使ってモナドトランスフォーマーをはがす関数を埋め込みます。hoistServer などについてはこちらを参考にしてください。リンク先の記事は最新版の Servant 0.12 に対応させました。

qiita.com

クエリーの発行

通常は runQuery' 関数に接続を渡して使用しますが、今回作ったハンドラーは Reader モナドで接続を持っていますので接続を渡す部分を隠すことができるはずです。次のようなラッパー関数を作りました。

runQuery'
  :: (ToSql SqlValue p, FromSql SqlValue a)
  => Query p a
  -> p
  -> Handler [a]
runQuery' q p = do
  pool <- lift ask
  withResource pool $ \conn ->
    liftIO $ R.runQuery' conn q p

モジュール構成

今のところ次のようなモジュール構成にして開発しています。

  • app
    • Main.hs
      • メイン関数を持つ
  • src
    • ServantHrr.hs
      • app/Main.hs から使われるインターフェースを公開する
      • Data
        • Common.hs
          • ハンドラーを横断して使用するデータ型を定義する
        • Book.hs
          • Book ハンドラーで使用するデータ型を定義する
        • Relation
          • Book.hs
            • データベースのテーブルと 1:1 対応し、HRR で生成される定義を持つ
      • Handler
        • Common.hs
          • ハンドラーを横断して使用する関数を定義する
        • Book.hs
          • Book ハンドラーを定義する
      • Api
        • Api 型を定義する
      • DataSource.hs
        • HRR 用
        • Secret.hs
          • DB 接続用のパスワードなど
          • VCS 管理下におかない

この記事は IIJ の執務時間を使って書かれました。