ServantはHaskellのWebフレームワークです。型でAPIを記述するのが特徴でAPIの型さえ書いてしまえばサーバーの実装は最小限の関数を書くだけで良くなります。(参考: 【型レベルWeb DSL】 Servantの紹介)
アプリケーションを作る時にログを吐くようにしておくことは大事です。monad-loggerはDebugやWarnなどのログレベルに応じて出力を制御できたりTemplate Haskellを使ってログが出力されているコードの位置を出力したり出来てとても便利です。
Servant 0.12 以降を対象としています。0.11 以前のバージョンについては以前の版を参照してください。
今回はServantとmonad-loggerを組み合わせて使う方法を紹介します。Servantはサーバーを実装するときにHandler a
(ExceptT ServantErr IO a
のnewtype)の値を返すように作る必要がありますがこれだとLoggingT
が入っていないのでそのままではログを吐くことが出来ません。そのためにまずは別のモナドトランスフォーマーで実装した後にモナド準同型と呼ばれる自然変換を利用して型を合わせるという技を使います。文字だけだとわけがわからないと思うので順を追って説明していきます。
今回作るWebサーバーは以下の様なものです
type API = Capture "anypath" Text :> Get '[PlainText] Text
/:anypath
にアクセスすると何かのplain text
が返って来ます。この時アクセスされた:anypath
を標準出力にログとして出していくことにします。
loggingServer :: ServerT API (LoggingT Handler)
loggingServer anypath = do
$(logInfo) anypath
pure "success"
これがサーバーの実装です。$(logInfo) anypath
でログを吐き、pure "success"
で"success"
という文字列を返しています。大事なのは型です。通常はServer API
(ServerT API Handler
のエイリアス)のような型を使うと思いますが今回はLoggingT
を使いたかったのでServerT Api (LoggingT Handler)
のようにしています。これでdo構文の中はLoggingT Handler a
と思って実装すれば良くなります。
実際にサーバーを動かすためにはServer API
という型が必要ですが手元にはまだServerT API (LoggingT Handler)
しかありません。そこで自然変換を用います。自然変換は圏論に出てくる概念で関手(Functor)から関手への射になります(参考: 自然変換)。これをHaskellで表すとforall a. f a -> g a
となります。
ServantではhoistServer :: HasServer api '[] => Proxy api -> (forall x. m x -> n x) -> ServerT api m -> ServerT api n
が用意されています。この第2引数が自然変換になっているのでここに適切な関数を渡せばよいことになります。例えばrunStdoutLoggingT :: MonadIO m => LoggingT m a -> m a
がちょうどその型に一致します。
main :: IO ()
main = do
putStrLn "Listening on port 8080"
let server = hoistServer api runStdoutLoggingT loggingServer
Warp.run 8080 $ serve api server
これがmain関数になります。
実際に動かしてみましょう!
Google Chrome が favicon.ico を読みに行ってるとこまでバッチリわかりますね!
以下に今回作ったコードの全体を載せておきます。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad.Logger
import Data.Text (Text)
import qualified Network.Wai.Handler.Warp as Warp
import Servant
type API = Capture "anypath" Text :> Get '[PlainText] Text
api :: Proxy API
api = Proxy
loggingServer :: ServerT API (LoggingT Handler)
loggingServer anypath = do
$(logInfo) anypath
pure "success"
main :: IO ()
main = do
putStrLn "Listening on port 8080"
let server = hoistServer api runStdoutLoggingT loggingServer
Warp.run 8080 $ serve api server
name: servant-logger-example
version: 0.1.0.0
build-type: Simple
cabal-version: >=1.10
executable app
hs-source-dirs: app
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: base
, text
, monad-logger
, warp
, servant-server >= 0.12
default-language: Haskell2010
ログを取るためにモナド準同型や自然変換といった概念が登場してきましたがモナドを乗り越えた人なら分かるとおり数学はプログラマの味方です。使いこなせればよりシンプルに強力なコードを書けるようになるでしょう。気になる人はmmorphというライブラリもチェックしてみて下さい!
この記事は第29回Haskellもくもく会@朝日ネットでの成果を元に書かれました。