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という型を定義する
monadを実装してApplication Monadにする
main関数内で、Envを作ってglobal環境として登録する
他のモナドと組み合わせてモナドスタックを作るようなことはしない
あとはHas型クラスパターンでenvの各fieldにアクセスできるような型クラスや関数を作る
その際に、できる限りIOなどが入れ込まないように注意する
ReaderTパターンの短所
ボイラプレートが多い
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
mrsekut.iconの前の誤解
「ReaderTパターンとは、ReaderTとIORefの組み合わせ」というものではない
そもそも可変参照はIORefである必要はない
TVarやIOでも良い
また、その組み合わせ自体が嬉しい点というわけでもない
ReaderTパターン#6134c86c198270000083e17cを読めば、いかに↑この理解が稚拙だったかわかる
グローバル変数が用意できればいい、ってだけの話でもない?
だってそれだったらそんなコトしなくても普通に
/LugendrePublic/ReaderT パターン(翻訳)の読みメモ
main関数で設定ファイルを読む
そして、main以外の部分に値を(明示的に,またはReaderTを経由して暗黙的に)渡す
リソースの初期化もmainの中で行う
数ジェネレータの初期化
ログメッセージの送信のためにHandleの獲得
データーベースプールの設定
一時ディレクトリを作りファイルにストアしたり
WriterTとStateTを避ける
普通に考えれば可変参照(e.g. IORef)よりも、WriterやStateを使ったほうが純粋になって良さそう
でも避けるべき、何故か?
/LugendrePublic/ReaderT パターン(翻訳)#5c4c7da48e79ae000050f693
Writer, Stateの問題点
ランタイム例外があると状態を失ってしまう
並行プログラミング時の結果が実装依存になる
WriterTにはスペースリークがある
ExceptTを避ける
どこからこの話出てきたんだ?mrsekut.icon
一般的にアプリケーションを作るためにはExceptは要るだろう?
けど、ReaderTパターンならExceptTを使わずに同様のことができるよ、という感じの文脈かな
ReaderTのみを使う
他のモナドを使用してモナド変換子を積み上げる必要もない
GHCの最適化もうまく効く
/LugendrePublic/ReaderT パターン(翻訳)#5c4c95c78e79ae000050f6ca
main内でEnvの初期化をしている
例としてEnvの中身は2種類
log用と、balance用
balanceが何なのかわからんがmrsekut.icon
問題点
modifyは、askでEnvの中身を全部取り入れている
modifyはbalanceのためだけの関数なので、実際にはlog用のEnvは不要
logSomething
modifyと同様
この問題点が問題点である理由
型から責務が読み取りづらい
テストがしづらい
この解決としてHas型クラスを使う
MonadReaderやその他のMonadThrowやMonadIOと言った他のmtl型クラスと相性が良く,関数が何を要求するのかを厳密に言及することができるようになる
/LugendrePublic/ReaderT パターン(翻訳)#5c4c969d8e79ae000050f6fc
HasLogとHasBalance型クラスを定義した
また、そのinstanceを2つ定義した
ボイラープレートは増えた
注目すべきは、modifyとlogSomethingの型クラス制約部分
modifyには、HasBalance制約がのみ入っているため(HasLog制約がないため)、balanceのみにアクセスできる
型を見ればそれがわかるし、
実際そういう制約があるので安全
テストも書きやすくなった
/LugendrePublic/ReaderT パターン(翻訳)#5c4c9b678e79ae000050f73d
このボイラープレートを端折るためにLensを使う
/LugendrePublic/ReaderT パターン(翻訳)#5c4c9b788e79ae000050f751
この部分
code:hs
makeLensesWith camelCaseFields ''Env
とかこの部分
code:hs
modify f = do
env <- ask
liftIO $ atomically $ modifyTVar' (env^.balance) f
天ぷらHaskellか...mrsekut.icon
これはEnvのrecordのネストが激しくなるほど嬉しくなる
しかしまだ問題がある
modifyの制約にMonadIOがあるのを取り除きたい
/LugendrePublic/ReaderT パターン(翻訳)#5c4ca1bb8945ba000003c93b
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がふさわしくない理由
https://github.com/purescript-halogen/purescript-halogen/issues/386
ReaderTパターンで言語処理系のenvを作る
pursの例
https://gist.github.com/rlucha/696ca604c9744ad11aff7d46b1706de7
参考
https://tyfkda.github.io/blog/2018/09/27/readert-pattern.html
https://mizunashi-mana.github.io/blog/posts/2020/01/use-reader-instead-of-state/
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
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
もうちょいやる
code: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
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
https://thomashoneyman.com/guides/real-world-halogen/push-effects-to-the-edges/