ReaderTパターン
理解度が甘いのでうまく要約できないが雰囲気はこんな感じmrsekut.icon
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だmrsekut.icon
参考
本家
なぜReader+IORefなのか、他の選択肢(State, Writer)は何故ダメなのか
解説を交えた小さな例
syntax highlightがないのでコードが読みづらい
mrsekut.iconの前の誤解
「ReaderTパターンとは、ReaderTとIORefの組み合わせ」というものではない
そもそも可変参照はIORefである必要はない
TVarやIOでも良い
また、その組み合わせ自体が嬉しい点というわけでもない
グローバル変数が用意できればいい、ってだけの話でもない?
だってそれだったらそんなコトしなくても普通に
main関数で設定ファイルを読む
そして、main以外の部分に値を(明示的に,またはReaderTを経由して暗黙的に)渡す
リソースの初期化もmainの中で行う
数ジェネレータの初期化
ログメッセージの送信のためにHandleの獲得
データーベースプールの設定
一時ディレクトリを作りファイルにストアしたり
WriterTとStateTを避ける
普通に考えれば可変参照(e.g. IORef)よりも、WriterやStateを使ったほうが純粋になって良さそう
でも避けるべき、何故か?
Writer, Stateの問題点
ランタイム例外があると状態を失ってしまう
並行プログラミング時の結果が実装依存になる
ExceptTを避ける
どこからこの話出てきたんだ?mrsekut.icon
一般的にアプリケーションを作るためにはExceptは要るだろう?
けど、ReaderTパターンならExceptTを使わずに同様のことができるよ、という感じの文脈かな
ReaderTのみを使う
他のモナドを使用してモナド変換子を積み上げる必要もない
GHCの最適化もうまく効く
main内でEnvの初期化をしている
例としてEnvの中身は2種類
log用と、balance用
balanceが何なのかわからんがmrsekut.icon
問題点
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を使う
この部分
code:hs
makeLensesWith camelCaseFields ''Env
とかこの部分
code: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.icon
これでうまくいくの不思議
この辺の記述がよくわからない
foo :: Monad m => Int -> m Double関数は純粋でないように見えるかもしれない
が,そうではない
「Monadの任意のインスタンス」という制約を関数に与えることによって,我々は「これは実際の副作用はもたない」と述べている
結局の所,上記の型はIdentityに単一化され,もちろんこれは純粋である
mを開くことで、「純粋なMonadの関数である」と見なせるのかmrsekut.icon
頭おかしい
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について
簡単な例
code:hs
import Data.IORef
import Control.Monad.Reader
import Control.Monad.State
dup :: ReaderT Env IO ()
dup = do
env <- ask
liftIO $ do
x <- readIORef env
writeIORef env (x ++ x)
main = do
runReaderT dup env
readIORef env
もうちょいやる
code:hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-} import Data.IORef
import Control.Monad.Reader
import Control.Monad
import Control.Monad.State
import Control.Monad.Trans
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
code: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