純粋な関数として定義できるんだけど内部でやってることが複雑な場合、何が起きてるかわからないと心配だからとログを吐く機能をつけると、その時点でそいつは IO
アクションになってしまう。ログを吐くという副作用を持つのだから IO
になるのは当たり前でそれを避けるべきではないのだけど、ログを吐かなくていいいシチュエーションでは、その計算を純粋な関数として使えたほうが理想的ではある。
そんなことを Identity
と 型クラス
使えば簡単にできるんじゃねと思いついたんだけど、 monad-logger でそもそも機能が提供されてた。
runLoggingT
や runNoLoggingT
でモナドに class MonadLogger
が持つロギング用のアクションを追加できるのだけど、前者はモナドが class MonadIO
のとき、後者は任意の class Monad
について使えるようインスタンスが定義されている。
以下の例で addM
は足し算するだけのアクションだが、引数をロギングするようになっている。 add
は addM
へ Identity
モナドを代入してそれを純粋な関数にしたもの1。
{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Monad.Logger
import Criterion
import Criterion.Main
import Data.Monoid ((<>))
import qualified Data.Text as TX
import Data.Functor.Identity
showT :: Show a => a -> TX.Text
showT = TX.pack . show
addM :: (MonadLogger m, Num a, Show a) => a -> a -> m a
addM x y = do
logDebugN $ "x=" <> showT x <> ", y=" <> showT y
return $ x + y
add :: (Num a, Show a) => a -> a -> a
add x y = runIdentity . runNoLoggingT $ addM x y
main :: IO ()
main = do
runStdoutLoggingT $ do
n <- addM 1 2
logDebugN $ showT (n :: Int)
defaultMain
[ bgroup "add"
[ bench "add" $ whnf (add 1) (2 :: Int)
, bench "(+)" $ whnf (1 +) (2 :: Int)
]
]
素の足し算と比べるとオーバヘッドはある。
$ stack ghc -- -o pure-logger pure-logger.hs
[1 of 1] Compiling Main ( pure-logger.hs, pure-logger.o )
Linking pure-logger ...
$ ./pure-logger
[Debug] x=1, y=2
[Debug] 3
benchmarking add/add
time 87.08 ns (83.77 ns .. 91.27 ns)
0.988 R² (0.975 R² .. 0.999 R²)
mean 84.59 ns (82.95 ns .. 87.74 ns)
std dev 7.355 ns (4.199 ns .. 12.95 ns)
variance introduced by outliers: 88% (severely inflated)
benchmarking add/(+)
time 22.31 ns (20.54 ns .. 23.96 ns)
0.970 R² (0.964 R² .. 0.980 R²)
mean 21.62 ns (20.51 ns .. 22.97 ns)
std dev 3.736 ns (3.185 ns .. 4.343 ns)
variance introduced by outliers: 97% (severely inflated)
が、 -O
でコンパイルしたところ、オーバヘッドはきれいに消え去った。やはり最適化がきちんと効くとGHCは強い。
$ stack ghc -- -O -o pure-logger pure-logger.hs
[1 of 1] Compiling Main ( pure-logger.hs, pure-logger.o )
Linking pure-logger ...
$ ./pure-logger
[Debug] x=1, y=2
[Debug] 3
benchmarking add/add
time 5.809 ns (5.663 ns .. 5.981 ns)
0.997 R² (0.995 R² .. 0.999 R²)
mean 5.728 ns (5.662 ns .. 5.835 ns)
std dev 277.3 ps (193.9 ps .. 404.2 ps)
variance introduced by outliers: 74% (severely inflated)
benchmarking add/(+)
time 6.036 ns (5.986 ns .. 6.096 ns)
0.999 R² (0.998 R² .. 0.999 R²)
mean 6.163 ns (6.090 ns .. 6.334 ns)
std dev 367.0 ps (221.5 ps .. 643.8 ps)
variance introduced by outliers: 81% (severely inflated)
これでログを吐きつつ計算するI/Oアクションと純粋な関数が、一切のオーバヘッドなしに同時に手に入った。