ReaderTパターン
アプリ全体で共有したい依存関係を、(関数引数ではなく)モナド経由で注入するためのパターン
前提として、ReaderT r m aはこういうイメージ
code:hs
newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
環境 r を読める m a、と解釈できる
なぜ使うのか?
アプリケーション全体で使うものを上からDIしたい
例えば、こういうモノを流し込みたい
DBコネクション
Logger
Config
単純に考えると、全部引数で渡すことになり辛い
code:hs
foo :: Config -> DB -> Logger -> IO ()
bar :: Config -> DB -> Logger -> IO ()
そこで、流し込むものをまとめたEnvを定義しすることで、
code:hs
data Env = Env
{ envConfig :: Config
, envDB :: DB
, envLogger :: Logger
}
type App = ReaderT Env IO
こうできる
code:hs
foo :: App ()
bar :: App ()
code:hs
foo = do
db <- asks envDB
...
最小構成の例
code:hs
data Env = Env
{ message :: String
}
type App = ReaderT Env IO
runApp :: Env -> App a -> IO a
runApp env app = runReaderT app env
hello :: App ()
hello = do
msg <- asks message
liftIO $ putStrLn msg
code:hs
main = runApp (Env "hello") hello
嬉しさ
DIできる
テスト時に入れ替えられる
code:hs
runReaderT app fakeEnv
依存が型で明示される
e.g. Appの中にいる限り、 DBやLoggerがあることが保証される
短所
ボイラプレートが多い
Envのfieldごとに型クラスを定義していくので、Envが大きいほどボイラプレートが増える
解決策
関連
ReaderTパターンからボイラープレートをなくす
3層に分けるアーキテクチャ
その内の1層でReaderTパターンを使う
実装例
これはHaskell Cakeの実装例だが、部分的にReaderTパターンも含まれている
NESエミュレータ
めちゃくちゃでかいEnvだmrsekut.icon
参考
本家
なぜReader+IORefなのか、他の選択肢(State, Writer)は何故ダメなのか
解説を交えた小さな例
syntax highlightがないのでコードが読みづらい
rootに近い部分でnewtype AppM = AppM ReaderT Env IOという型を定義する
before
code:hs
foo :: App ()
foo = do
db <- asks envDB
liftIO $ query db
fooはEnvの内部構造を知っているので、Envの構造が変わるとfooも壊れる
after
code:hs
foo :: (MonadReader env m, HasDB env, MonadIO m) => m ()
foo = do
db <- asks getDB
liftIO $ query db
fooはEnvを知らない
WriterTとStateTを避ける
普通に考えれば可変参照(e.g. IORef)よりも、WriterやStateを使ったほうが純粋になって良さそう
でも避けるべき、何故か?
Writer, Stateの問題点
ランタイム例外があると状態を失ってしまう
並行プログラミング時の結果が実装依存になる
Stateモナドと、IORefモナドのパフォーマンスの比較
ReaderとIORefを組み合わせることで、
Readr→envを持ち回さなくていい
IORef→envの書き換えができる