ReaderTパターン
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が大きいほどボイラプレートが増える
解決策
Lensを使う ref
Capabilityパターンを使う
関連
Capabilityパターン
ReaderTパターンからボイラープレートをなくす
Three Haskell Cake
3層に分けるアーキテクチャ
その内の1層でReaderTパターンを使う
実装例
purescript-halogen-realworld
これはHaskell Cakeの実装例だが、部分的にReaderTパターンも含まれている
Store型がEnvに相当する
hnes
NESエミュレータ
Emulator型がEnvに相当する
めちゃくちゃでかいEnvだmrsekut.icon
参考
The ReaderT Design Pattern
本家
なぜReader+IORefなのか、他の選択肢(State, Writer)は何故ダメなのか
解説を交えた小さな例
syntax highlightがないのでコードが読みづらい
/LugendrePublic/ReaderT パターン(翻訳)
#WIP
rootに近い部分でnewtype AppM = AppM ReaderT Env IOという型を定義する
monadを実装してApplication Monadにする
あとはHas Patternでenvの各fieldにアクセスできるような型クラスや関数を作る
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を知らない
/LugendrePublic/ReaderT パターン(翻訳)の読みメモ
WriterTとStateTを避ける
普通に考えれば可変参照(e.g. IORef)よりも、WriterやStateを使ったほうが純粋になって良さそう
でも避けるべき、何故か?
/LugendrePublic/ReaderT パターン(翻訳)#5c4c7da48e79ae000050f693
Writer, Stateの問題点
ランタイム例外があると状態を失ってしまう
並行プログラミング時の結果が実装依存になる
WriterTにはスペースリークがある
https://mizunashi-mana.github.io/blog/posts/2020/01/use-reader-instead-of-state/
Stateモナドと、IORefモナドのパフォーマンスの比較
ReaderとIORefを組み合わせることで、
Readr→envを持ち回さなくていい
IORef→envの書き換えができる
https://thomashoneyman.com/guides/real-world-halogen/push-effects-to-the-edges/