ReaderTパターン
理解度が甘いのでうまく要約できないが雰囲気はこんな感じ

ReaderTとEnvを使ってアプリケーション全体で使うような環境変数を管理する
つまり、global変数を純粋さを保って扱う方法
ここでの環境変数とは、本番と開発で異なる処理が欲しい部分のflugのようなもの
例えば
Logの出力レベルとか、
baseURLとか、
DBアクセスとか
ログイン状態で開発できるようにするためのcurrentUserとか
こういった環境変数の渡し方はいくつか考えられるが、その中でも筋の良い方法の1つがReaderTパターン
Envとは、それらのglobalな環境変数を管理する自分で定義したrecord
状態を扱うためには、 IORef
, IO
, TVar
のような可変参照を使う
StateやWriterではなく。
rootに近い部分で newtype AppM = AppM ReaderT Env IO
という型を定義する
main関数内で、Envを作ってglobal環境として登録する
他のモナドと組み合わせてモナドスタックを作るようなことはしない
その際に、できる限りIOなどが入れ込まないように注意する
ReaderTパターンの短所
ボイラプレートが多い
Envのfieldごとに型クラスを定義していくので、Envが大きいほどボイラプレートが増える
解決策
関連
ReaderTパターンからボイラープレートをなくす
3層に分けるアーキテクチャ
その内の1層でReaderTパターンを使う
実装例
これはHaskell Cakeの実装例だが、部分的にReaderTパターンも含まれている
NESエミュレータ
めちゃくちゃでかいEnvだ

参考
本家
なぜReader+IORefなのか、他の選択肢(State, Writer)は何故ダメなのか
解説を交えた小さな例
syntax highlightがないのでコードが読みづらい

の前の誤解
「ReaderTパターンとは、ReaderTとIORefの組み合わせ」というものではない
そもそも可変参照はIORefである必要はない
TVarやIOでも良い
また、その組み合わせ自体が嬉しい点というわけでもない
グローバル変数が用意できればいい、ってだけの話でもない?
だってそれだったらそんなコトしなくても普通に
main
関数で設定ファイルを読む
そして、 main
以外の部分に値を(明示的に,または ReaderT
を経由して暗黙的に)渡す
リソースの初期化も main
の中で行う
数ジェネレータの初期化
ログメッセージの送信のために Handle
の獲得
データーベースプールの設定
一時ディレクトリを作りファイルにストアしたり
WriterTとStateTを避ける
普通に考えれば可変参照(e.g. IORef)よりも、WriterやStateを使ったほうが純粋になって良さそう
でも避けるべき、何故か?
Writer, Stateの問題点
ランタイム例外があると状態を失ってしまう
並行プログラミング時の結果が実装依存になる
ExceptTを避ける
どこからこの話出てきたんだ?

一般的にアプリケーションを作るためにはExceptは要るだろう?
けど、ReaderTパターンならExceptTを使わずに同様のことができるよ、という感じの文脈かな
ReaderTのみを使う
他のモナドを使用してモナド変換子を積み上げる必要もない
GHCの最適化もうまく効く
main内でEnvの初期化をしている
例としてEnvの中身は2種類
log用と、balance用
balanceが何なのかわからんが

問題点
modify
は、 ask
でEnvの中身を全部取り入れている
modify
はbalanceのためだけの関数なので、実際にはlog用のEnvは不要
logSomething
modify
と同様
この問題点が問題点である理由
型から責務が読み取りづらい
テストがしづらい
この解決としてHas型クラスを使う
MonadReader
やその他の MonadThrow
や MonadIO
と言った他のmtl型クラスと相性が良く,関数が何を要求するのかを厳密に言及することができるようになる
HasLog
と HasBalance
型クラスを定義した
また、そのinstanceを2つ定義した
ボイラープレートは増えた
注目すべきは、 modify
と logSomething
の型クラス制約部分
modify
には、 HasBalance
制約がのみ入っているため( HasLog
制約がないため)、balanceのみにアクセスできる
型を見ればそれがわかるし、
実際そういう制約があるので安全
テストも書きやすくなった
このボイラープレートを端折るためにLensを使う
この部分
hs makeLensesWith camelCaseFields ''Env
とかこの部分
hs modify f = do
env <- ask
liftIO $ atomically $ modifyTVar' (env^.balance) f
これは Env
のrecordのネストが激しくなるほど嬉しくなる
しかしまだ問題がある
modify
の制約に MonadIO
があるのを取り除きたい
lens使ってないver
MonadBalance
型クラスが新たに定義された
AppM
を、↑これのinstanceにする
その際に、 MonadIO
の型クラス制約も付ける
そすることで、 modify
自体からは MonadIO
が消える
これまだ完全には理解できていない

これでうまくいくの不思議
この辺の記述がよくわからない
foo :: Monad m => Int -> m Double
関数は純粋でないように見えるかもしれない
が,そうではない
「 Monad
の任意のインスタンス」という制約を関数に与えることによって,我々は「これは実際の副作用はもたない」と述べている
結局の所,上記の型は Identity
に単一化され,もちろんこれは純粋である
m
を開くことで、「純粋なMonadの関数である」と見なせるのか

頭おかしい
parseInt :: MonadThrow m => Text -> m Int
はどうだろうか?
あなたは,「それは純粋でなく,ランタイム例外を投げる」と思うかもしれない.
しかしながら,この型は parseInt :: Text -> Maybe Int
に単一化され,もちろんこれは純粋である.
状態がネストしたりと、程々に複雑で、変更が局所的な場合に用いると良い
ref
stateがふさわしくない理由
pursの例
参考
Stateモナドと、IORefモナドのパフォーマンスの比較
↓この辺はReaderTの本質とはややそれるが、これはこれでまとめといても良いものな気もする
めちゃくちゃ単純化したら
Readerモナドのみでは、環境から読み込むことしか出来ない
しかし、evalではAssignなど環境を書き換える必要も出てくる
そこでIORefと組み合わせる
ReaderとIORefを組み合わせることで、
Readr→envを持ち回さなくていい
IORef→envの書き換えができる
IORef & Readerについて
簡単な例
hsimport Data.IORef
import Control.Monad.Reader
import Control.Monad.State
type Env = IORef [Int]
dup :: ReaderT Env IO ()
dup = do
env <- ask
liftIO $ do
x <- readIORef env
writeIORef env (x ++ x)
main = do
env <- newIORef [1111]
runReaderT dup env
readIORef env
もうちょいやる
hs{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Data.IORef
import Control.Monad.Reader
import Control.Monad
import Control.Monad.State
import Control.Monad.Trans
type Env = IORef [Int]
newtype Eval a = Eval (ReaderT Env IO a)
deriving ( Functor
, Applicative
, Monad
, MonadReader Env
, MonadIO
)
dup :: Eval ()
dup = do
env <- ask
-- 一行で書くなら liftIO $ modifyIORef env ((:) 3)
xs <- liftIO $ readIORef env
liftIO $ writeIORef env (3:xs)
runEval :: Eval a -> Env -> IO a
runEval (Eval m) = runReaderT m
main = do
env <- newIORef []
runEval dup env
readIORef env
deriving (...)
の
...
の部分は
mtlパッケージによるもの
s <- IORef a
から a
はどうやれば取り出すことができる?
purs
purs(hs)module Env where
import Prelude
import Control.Monad.Reader.Class (class MonadAsk)
import Control.Monad.Reader.Trans (ReaderT, ask, runReaderT)
import Data.List (List(..), (:))
import Effect (Effect)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Ref as Ref
type Environment = Ref.Ref (List Int)
newtype Env a = Env (ReaderT Environment Effect a)
derive newtype instance bindEnv ∷ Bind Env
derive newtype instance monadAskEnv :: MonadAsk Environment Env
derive newtype instance monadEffectEnv :: MonadEffect Env
dup :: Env Unit
dup = do
env <- ask
-- 一行で書くなら liftEffect $ Ref.modify_ ((:) 3) env
xs <- liftEffect $ Ref.read env
liftEffect $ Ref.write (3:xs) env
runEval :: ∀ a. Env a -> Environment -> Effect a
runEval (Env m) = runReaderT m
main :: Effect (List Int)
main = do
env <- Ref.new Nil
runEval dup env
Ref.read env