generated at
ReaderTパターン
理解度が甘いのでうまく要約できないが雰囲気はこんな感じmrsekut
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 という型を定義する
monadを実装してApplication Monadにする
main関数内で、Envを作ってglobal環境として登録する
他のモナドと組み合わせてモナドスタックを作るようなことはしない
あとはHas型クラスパターンでenvの各fieldにアクセスできるような型クラスや関数を作る
その際に、できる限りIOなどが入れ込まないように注意する



ReaderTパターンの短所
ボイラプレートが多い
Envのfieldごとに型クラスを定義していくので、Envが大きいほどボイラプレートが増える
解決策
Lensを使う ref


関連
ReaderTパターンからボイラープレートをなくす
3層に分けるアーキテクチャ
その内の1層でReaderTパターンを使う


実装例
これはHaskell Cakeの実装例だが、部分的にReaderTパターンも含まれている
Store型がEnvに相当する
NESエミュレータ
Emulator型がEnvに相当する
めちゃくちゃでかいEnvだmrsekut


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






mrsekutの前の誤解
「ReaderTパターンとは、ReaderTとIORefの組み合わせ」というものではない
そもそも可変参照はIORefである必要はない
TVarやIOでも良い
また、その組み合わせ自体が嬉しい点というわけでもない
ReaderTパターン#6134c86c198270000083e17cを読めば、いかに↑この理解が稚拙だったかわかる



グローバル変数が用意できればいい、ってだけの話でもない?
だってそれだったらそんなコトしなくても普通に


main 関数で設定ファイルを読む
そして、 main 以外の部分に値を(明示的に,または ReaderT を経由して暗黙的に)渡す
リソースの初期化も main の中で行う
数ジェネレータの初期化
ログメッセージの送信のために Handle の獲得
データーベースプールの設定
一時ディレクトリを作りファイルにストアしたり
WriterTとStateTを避ける
普通に考えれば可変参照(e.g. IORef)よりも、WriterやStateを使ったほうが純粋になって良さそう
でも避けるべき、何故か?
Writer, Stateの問題点
ランタイム例外があると状態を失ってしまう
並行プログラミング時の結果が実装依存になる
WriterTにはスペースリークがある
ExceptTを避ける
どこからこの話出てきたんだ?mrsekut
一般的にアプリケーションを作るためにはExceptは要るだろう?
けど、ReaderTパターンならExceptTを使わずに同様のことができるよ、という感じの文脈かな
ReaderTのみを使う
他のモナドを使用してモナド変換子を積み上げる必要もない
GHCの最適化もうまく効く
main内でEnvの初期化をしている
例としてEnvの中身は2種類
log用と、balance用
balanceが何なのかわからんがmrsekut
問題点
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 が消える
これまだ完全には理解できていないmrsekut
これでうまくいくの不思議
この辺の記述がよくわからない
foo :: Monad m => Int -> m Double 関数は純粋でないように見えるかもしれない
が,そうではない
Monad の任意のインスタンス」という制約を関数に与えることによって,我々は「これは実際の副作用はもたない」と述べている
結局の所,上記の型は Identity に単一化され,もちろんこれは純粋である
m を開くことで、「純粋なMonadの関数である」と見なせるのかmrsekut
頭おかしい
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について
簡単な例
hs
import 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
GeneralizedNewtypeDerivingを使うことでnewtypeした型に対して、自動導出できる
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