ReaderT パターン(翻訳)
Haskellの "デザインパターン"についての質問をオンラインで受け取るか読むことがよくある.一般的な回答は,Haskellにはそれらはない,である.多くの言語はデザインパターンを介して問題に対処するものだ.Haskellでは,それらの問題は言語機能(組み込みの不変性,ラムダ,遅延性など)を介して対処する.しかしながら,Haskellのデザインパターンと大まかに言えるだろう,構造化プログラムに関するいくつかのハイレベルな指針を与える余地がまだあると私は考える.
私が今日説明しようとしているデザインパターンは,少なくとも数年の間,非公式の議論で「ReaderTパターン」として言及してきたものである.私はそれをYesodのHandler型の設計の基礎として使っている.それはStackコードベースの大部分を表現している.そして私は友人,同僚,顧客に定期的にこれを勧めている.
とは言っても,このパターンはHaskellの世界では共通の認識があるわけではなく,多くの人々が自分たちのコードを異なった方法で設計している.だから,私が書いた他の記事のように,これは非常に独断的だが,あくまで私の個人的な,そしてFP Completeの推奨するベストプラクティスであることを忘れないよう.
このデザインパターンを要約してから,詳細と例外を調べてみよう.
アプリケーションはコアのデータ型を定義する必要がある(必要に応じてそれはEnvと呼ばれる).
このデータ型は,(ロギング機能やデータベースアクセスのような)モック可能なすべてのランタイム設定とグローバル機能を含む.
いくつかの可変状態を持たなければならない場合は,可変参照としてのEnvにそれをいれよう(IORef,TVarなど).
一般に,アプリケーションコードはReaderT Env IOに内在する.望むなら,type App = ReaderT Env IOと定義するか,さもなくば,ReaderTを直接使うのではなく,newtypeラッパを使うこと.
場合によっては,追加のモナド変換子を使うことができるが,それはアプリケーションの小さなサブセットに限られる.これらのサブセットが純粋なコードである場合はそれが最善である.
オプション:Appデータ型を直接使用する代わりに,MonadReaderやMonadIOのようなmtlスタイルの型クラスで関数を作成する.これにより,IOと可変参照によって投げ捨てるように言ったように聞こえる純粋さの一部を回復できる.
これらは一気に理解するには長い.(mtl型クラスのように)それのうちのいくつかは不明瞭かもしれない.そして他の部分(特にその可変参照の部分)はおそらく完全に間違っていると感じられるだろう.これらの主張をありだと思ってもらえるように,ニュアンスを説明するとしよう.
いい感じのグローバル
簡単な部分をざっと説明しよう.グローバル変数は悪く,可変グローバルはもっと悪い.ロギングレベルを設定したいアプリケーションがあるとしよう(たとえば,DEBUGレベルのメッセージを表示すべきか,握りつぶすべきか?).Haskellでは,以下の3つの方法が一般的である.
コンパイル時フラグを使用してどのロギングコードを実行時ファイルに含めるのか制御する
unsafePerformIOを使用して,設定ファイル(または環境変数)を読み込み,グローバルな値を定義する.
main関数で設定ファイルを読み,それからコードのmain以外の部分に値を(明示的に,またはReaderTを経由して暗黙的に)渡す.
(1)は魅力的だが,経験からするとそれはひどい解決策である.コードベースで条件付きコンパイルを行うたびに,ビルド失敗の可能性があるフラクタルが追加される.5つの条件がある場合は,32(2 ^ 5)種類の可能なビルド設定がある.これら32種類の設定すべてに対して,正しいimport宣言のセットがあることを確認しているか?これをやるのはただただ苦痛である.さらに,デバッグ時にデバッグ情報が不要になることを本当にコンパイル時に決定したいだろうか?私はむしろ,デバッグ中に多くの情報を得るために,コンフィグファイルのfalseをtrueに反転してアプリを再起動することができたほうが遥かに嬉しい.
(ちなみに,これよりも優れているのは,デバッグレベルを変更するために実行中にプロセスに信号を送る機能だが,ここでは言及しない.ReaderT+可変変数パターンは,これを達成するためのベストな方法の1つである.)
それでは,条件付きでコンパイルすべきではないということに関してあなたは私と同意した.とはいえ、もうアプリケーション全体を作成したので、なんらかの設定値をあちこちに引き回すために書き換えなきゃいけないことを躊躇するだろう.わかる(わかる).実際辛い.だから,ファイルは一回しか読まないし,ランタイム全体で純粋っぽい値だし,全体的に安全そうだし,もうunsafePerformIOを使っちゃえと思うわけだ.しかしながら:
これで,例外が発生する場所についてわずかなレベルの不確定性がある.設定ファイルが見つからないか,無効な場合,どこから例外がスローされるのだろうか?アプリの起動時にすぐに発生してくれる方がはるかに嬉しい.
(残りのコードよりも失敗する可能性が高いと分かっているので)より多いデバッグ情報を使用してアプリケーションの小さな一部分を実行したいとしよう.基本的には,これは全然できない.
あなたがunsafePerformIOを使うたびに,子猫が一匹死にます.
苦痛をこらえ,Envデータ型を定義し,設定値を入れて,あなたのアプリケーションのあちこちに引き回すときがきた.最初からそのようにアプリケーションをデザインしていたなら,問題なくいい感じになる.アプリケーションの開発の後半でそれを行うのは確かに辛いが,以下に述べるいくつかの事項はこのつらみを軽減できる.そして,unsafePerformIO周りでのデータの競合状態に直面するよりも,機械的なコードの書き換えという少しの苦痛を背負ったほうが絶対に良い.これはHaskellであることを覚えておこう.つまり,私達はランタイムよりもコンパイル時に苦しみたいと思うということだ.
悲運を受け入れ,(3)にオールインしたら,これからは何不自由ない贅沢な暮らしが待っている:
いくつかの新しい設定値を渡したい?簡単,ただEnv型のフィールドを増やすだけだ.
ログレベルを一時的に上げたい?localを使えば人生バラ色だ.
CPPコード(1)やグローバル変数(2)のような醜いハックに頼る可能性ははるかに低くなる.「「「正しい方法」」」に潜む苦痛を取り除いたからだ.
リソースの初期化
設定値を読む場合は良かったが,もっと良いパターンとしていくつかのリソースの初期化がある.乱数ジェネレータを初期化したり,ログメッセージの送信のためにHandleを獲得したり,データーベースプールを設定したり,一時ディレクトリを作りファイルにストアしたりしたいとしよう.これらすべて,なんらかのグローバルの位置からよりmainの中でするほうがはるかに論理的である.
これらの種類の初期化におけるグローバル変数によるアプローチの利点の一つは,値を使う最初の一回まで初期化を遅らせられることである.これは,いくつかのリソースを必要としないことがあると考えられる場合には嬉しい.しかし,それらがほしいなら,runOnceのようなアプローチを使っても良い.
WriterTとStateTを避ける
一体どうして最後の手段以外のものとして可変参照を推奨するのだろうか.私たちは皆,Haskellの純粋性が最優先事項であり,可変性が悪魔であることを知っている.それに,アプリケーションで時間とともに変える必要があるいくつかの値を持ちたいときには,我々はWriterTやStateTと呼ばれる素晴らしいものを知っている.なぜそれらを使わないのか?
実際,初期のバージョンのYesodはまさにそうだった.Yesodはあなたがユーザセッション値を変更して,レスポンスヘッダを設定すできるように,Handler内でStateT的なアプローチを使っていた.しかし,ずいぶん前に可変参照に切り替えた.以下が理由だ.
例外下での生存
ランタイム例外があると,WriterTやStateTにある状態を失うことになる.可変参照ではそうではない.ランタイム例外がスローされる前の最後の有効な状態を読み取ることができる.これは,レスポンスがnotFoundのように失敗した場合でも,レスポンスヘッダを設定できるようにするために,Yesodでとても便利に使用されている.
偽の純粋性
私たちはWriterTやStateT純粋であるといい,厳密にはそうだ.しかし,正直に言おう.アプリケーションが完全にStateTに内在しているならば,純粋なコードに期待する,ミュータブルな変数の使用を制限するという性質は持たない.ごまかさずに,可変変数を持っていると言おう!
並行性
put 4 >> concurrently (modify (+ 1)) (modify (+ 2)) >> getの結果は何ですか?あなたはそれが7になるだろうと言うことを望むかもしれないが,それは絶対にない.
考えられる選択肢は,StateTが提供する状態に関してconcurrentlyがどのように実装されているかに依り,4,5,6である.信じられない?次を試してみて.
code:fpc01.hs
-- stack --resolver lts-8.12 script
import Control.Concurrent.Async.Lifted
import Control.Monad.State.Strict
main :: IO ()
main = execStateT
(concurrently (modify (+ 1)) (modify (+ 2)))
4 >>= print
問題は,状態を親スレッドから両方の子スレッドに複製する必要があり,その後どの子状態が生き残るかを任意に選ぶ必要があることである.または,必要に応じて,両方の子状態を破棄して元の親状態を続けることもできる(ちなみに,このコードがコンパイルされることが悪いことだと言うなら,私もそのとおりだと思うし,Control.Concurrent.Async.Lifted.Safeを使うことを提案する).
異なるスレッド間での可変状態の処理は難しい問題だが,StateTはそれを隠すことはできても,問題を解決することはできない.可変変数を使用すると,これについて考えることを余儀なくされる.「どんな意味論が欲しい?IORefを使用し,atomicModifyIORefに固執する必要があるか?TVarを使うべきか?」これらは妥当な疑問であり,そして私達が考察することを余儀なくされるものである.TVarのようなアプローチの場合,
code:fpc02.hs
-- stack --resolver lts-8.12 script
{-# LANGUAGE FlexibleContexts #-} import Control.Concurrent.Async.Lifted.Safe
import Control.Monad.Reader
import Control.Concurrent.STM
modify :: (MonadReader (TVar Int) m, MonadIO m)
=> (Int -> Int)
-> m ()
modify f = do
ref <- ask
liftIO $ atomically $ modifyTVar' ref f
main :: IO ()
main = do
ref <- newTVarIO 4
runReaderT (concurrently (modify (+ 1)) (modify (+ 2))) ref
readTVarIO ref >>= print
WriterTは壊れている
Gabriel Gonzalezが示しているように,正格なWriterTでさえスペースリークがあることを忘れてはいけない. 但し書き
私は時々未だにStateTとWriterTを使用する.その代表的な例がYesod のWidgetTだ.これは基本的にはHandlerTの上にWriterTが居座っている.そういった文脈では意味がある.なぜなら,
可変状態は,アプリケーションの小さなサブセットに対して変更されることが予想される.
ウィジェットを構築しながら副作用を実行することはできるが,ウィジェットの構築自体は道徳的に純粋なアクティビティである.
例外時に状態を生き残らせる必要はない.何かがうまくいかない場合は,代わりにエラーページを送り返す.
ウィジェットを作成するときに並行性を使用する適切な理由はない.
私のスペースリークの懸念にもかかわらず,私は徹底的にWriterTとその代替案に対してベンチマークを行い,そしてWriterTがこのユースケースのための最速であることがわかった(数字は推論を破った).
この規則の他の大きな例外は純粋なコードだ.アプリケーションのサブセットにIOを実行できないがある種の可変状態を必要とするなら,絶対に,100%,StateTを使おう.
ExceptTを避ける
私はすでに,IO上のExceptTが悪い考えであることを断言している.手短に言うと,IOは例外がいつでもスローされる可能性があるという契約であり,実際にはExceptTは考えられる例外を文書化していないため,誤解を招く.詳細についてはそのブログ記事に書いてある. 私は,ExceptTを適用したStateTやWriterTのいくつかの欠点も同じなので,この話をここに書き直している.例えば,どのようにしてExceptTで並行処理を処理するか?実行時例外では,動作は明らかである.concurrentlyの使用時に,いずれかの子スレッドが例外をスローすると,もう一方のスレッドが強制終了され,親で例外がスローされる.どのような振る舞いをExceptTに望む?
繰り返すが,純粋なコードでStateTを使用すべきなのと同様に,実行時例外が契約の一部ではない場合は,純粋なコードでExceptT使用しても良い.しかし,一旦主なアプリケーションの変換子からStateT, WriterT, ExceptTを排除したなら,残されるのは…
ReaderTだけ
そして今,あなたは私がこれを「ReaderTデザインパターン」と呼ぶ理由を知っている.ReaderTは前述の他の3つの変換子よりも大きな利点をもつ.変更可能な状態はない.これは単にすべての関数に追加のパラメータを渡すのに便利な方法に過ぎない.そしてそのパラメータに可変参照が含まれていても,そのパラメータ自体は完全に不変である.とすれば,
並行性について述べた状態上書きの問題をすべて無視できる.上記の例で.Safeモジュールがどのように使用できたかに注目しよう.これは,ReaderTによって同時実行するのが実際に安全であるためである.
同様に、monad-unliftライブラリパッケージを使うことができる.
モナド変換子の深い積み重なりは紛らわしい.たった1つの変換子にまとめれば,複雑さが大幅に軽減される.
それはあなたにとってのみ単純なのではない.GHCにとっても簡単で,変換子の深さが5のコードよりもReaderT1層のコードを最適化する方がはるかにうまい傾向がある.
ちなみに,一度ReaderTを無批判に受け入れたら,それを完全に捨てて,手動で自分のEnvを渡して回ることができる.私たちのほとんどはそれをしていない.なぜならそれはマゾっぽい感じがするからだ(logDebugの呼び出しのたびにロギング関数を得られる場所を教えなければならないと想像してみよう).しかし,変換子の理解を必要としない,より単純なコードベースを作成することも、今や手の届く範囲内だ.
Has型クラスパターン
ロギング関数を含むように,上記の可変関数の例を拡張するとしてみよう.これは次のようになるかもしれない.
code:fpc03.hs
-- stack --resolver lts-8.12 script
{-# LANGUAGE FlexibleContexts #-} import Control.Concurrent.Async.Lifted.Safe
import Control.Monad.Reader
import Control.Concurrent.STM
import Say
data Env = Env
{ envLog :: !(String -> IO ())
, envBalance :: !(TVar Int)
}
modify :: (MonadReader Env m, MonadIO m)
=> (Int -> Int)
-> m ()
modify f = do
env <- ask
liftIO $ atomically $ modifyTVar' (envBalance env) f
logSomething :: (MonadReader Env m, MonadIO m)
=> String
-> m ()
logSomething msg = do
env <- ask
liftIO $ envLog env msg
main :: IO ()
main = do
ref <- newTVarIO 4
let env = Env
{ envLog = sayString
, envBalance = ref
}
runReaderT
(concurrently
(modify (+ 1))
(logSomething "Increasing account balance"))
env
balance <- readTVarIO ref
sayString $ "Final balance: " ++ show balance
この例に対するあなたの最初の反応はおそらく,アプリケーションのためにEnvデータ型を定義するのはオーバーヘッドやボイラープレートのようにみえるというのだろう.そのとおりである.とはいえ,私が上で言ったように,より良い長期のアプリケーション開発のプラクティス用意するために最初の段階でつらみを受け入れたほうが良い.さぁ,このパターンに倍プッシュだ......
このコードにはもっと大きな問題がある.密結合すぎる.modify関数はEnvの値を全部取り入れているが,ロギング関数はまったく使っていない.同様に,logSomethingもEnvが提供する可変変数を全く使っていない.関数にあまりにも多くの状態を公開するのは悪いことである.
型シグネチャから,コードが何をしているかについての情報を得ることができない.
テストするのはもっと難しい.modifyが正しく動いているかを確認するために,我々はあるダミーのロギング関数を提供する必要がある.
ということでこのボイラープレートに倍プッシュし,Has型クラスのテクを使おう.
これはMonadReaderやその他のMonadThrowやMonadIOと言った他のmtl型クラスと相性が良く,関数が何を要求するのかを厳密に言及することができるようになる.ただし,前もって,大量の型クラスを定義するという犠牲を払う必要がある.これがどのようになるか見てみよう.
code:fpc04.hs
-- stack --resolver lts-8.12 script
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} import Control.Concurrent.Async.Lifted.Safe
import Control.Monad.Reader
import Control.Concurrent.STM
import Say
data Env = Env
{ envLog :: !(String -> IO ())
, envBalance :: !(TVar Int)
}
class HasLog a where
getLog :: a -> (String -> IO ())
instance HasLog (String -> IO ()) where
getLog = id
instance HasLog Env where
getLog = envLog
class HasBalance a where
getBalance :: a -> TVar Int
instance HasBalance (TVar Int) where
getBalance = id
instance HasBalance Env where
getBalance = envBalance
modify :: (MonadReader env m, HasBalance env, MonadIO m)
=> (Int -> Int)
-> m ()
modify f = do
env <- ask
liftIO $ atomically $ modifyTVar' (getBalance env) f
logSomething :: (MonadReader env m, HasLog env, MonadIO m)
=> String
-> m ()
logSomething msg = do
env <- ask
liftIO $ getLog env msg
main :: IO ()
main = do
ref <- newTVarIO 4
let env = Env
{ envLog = sayString
, envBalance = ref
}
runReaderT
(concurrently
(modify (+ 1))
(logSomething "Increasing account balance"))
env
balance <- readTVarIO ref
sayString $ "Final balance: " ++ show balance
なんてこったボイラープレートだ!そう,型シグネチャは長くなり,まるっきり同じようなインスタンスが書き込まれる.しかし,我々の型シグネチャは非常に有益になり,関数を簡単にテストすることができる.例えば,
code:fpc05.hs
main :: IO ()
main = hspec $ do
describe "modify" $ do
it "works" $ do
var <- newTVarIO (1 :: Int)
runReaderT (modify (+ 2)) var
res <- readTVarIO var
res shouldBe 3
describe "logSomething" $ do
it "works" $ do
var <- newTVarIO ""
let logFunc msg = atomically $ modifyTVar var (++ msg)
msg1 = "Hello "
msg2 = "World\n"
runReaderT (logSomething msg1 >> logSomething msg2) logFunc
res <- readTVarIO var
res shouldBe (msg1 ++ msg2)
そして,これらすべての型クラスを手動で定義することが悩ましいか,ライブラリの大ファンであれば,Lensを自由に使用することができる.
code:fpc06.hs
-- stack --resolver lts-8.12 script
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FunctionalDependencies #-} import Control.Concurrent.Async.Lifted.Safe
import Control.Monad.Reader
import Control.Concurrent.STM
import Say
import Control.Lens
import Prelude hiding (log)
data Env = Env
{ envLog :: !(String -> IO ())
, envBalance :: !(TVar Int)
}
makeLensesWith camelCaseFields ''Env
modify :: (MonadReader env m, HasBalance env (TVar Int), MonadIO m)
=> (Int -> Int)
-> m ()
modify f = do
env <- ask
liftIO $ atomically $ modifyTVar' (env^.balance) f
logSomething :: (MonadReader env m, HasLog env (String -> IO ()), MonadIO m)
=> String
-> m ()
logSomething msg = do
env <- ask
liftIO $ (env^.log) msg
main :: IO ()
main = do
ref <- newTVarIO 4
let env = Env
{ envLog = sayString
, envBalance = ref
}
runReaderT
(concurrently
(modify (+ 1))
(logSomething "Increasing account balance"))
env
balance <- readTVarIO ref
sayString $ "Final balance: " ++ show balance
Envに不変のconfigスタイルのデータが一つも含まれていない場合,Lensアプローチの利点はそれほど明白ではない.しかし,深くネストされた設定値があり,特にアプリケーション全体でその中のいくつかの値をlocalを使って少しだけ変えたい場合は,Lensアプローチはうまくいくだろう.
要約すると,このアプローチは実際に我慢することと,最初のつらみとボイラープレートを甘んじて受け入れることである.私はあなたがアプリケーション開発の間にこのパターンから得る無数の利益が,それらを受け入れる価値が十分にあると言える.覚えておこう.一回前払いすれば,毎日報酬を受け取れるのだ.
純粋性を取り戻せ
我々のmodify関数にMonadIO制約があるのは残念である.実際の実装では副作用を実行する(具体的に言えば,TVarの読み書き)ためにIOが必要だが,その関数のすべての呼び出しに,「ミサイルを打ったり,さらに悪いことに,ランタイム例外を投げることを含む,任意の副作用を実行する権利を持つ」という宣言を汚染させる.ある程度の純粋性を取り戻すことができるのだろうか?答えはイエスである.ただ,それを行うには,もう少しボイラープレートが必要になる.
code: fpc07.hs
-- stack --resolver lts-8.12 script
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} import Control.Concurrent.Async.Lifted.Safe
import Control.Monad.Reader
import qualified Control.Monad.State.Strict as State
import Control.Concurrent.STM
import Say
import Test.Hspec
data Env = Env
{ envLog :: !(String -> IO ())
, envBalance :: !(TVar Int)
}
class HasLog a where
getLog :: a -> (String -> IO ())
instance HasLog (String -> IO ()) where
getLog = id
instance HasLog Env where
getLog = envLog
class HasBalance a where
getBalance :: a -> TVar Int
instance HasBalance (TVar Int) where
getBalance = id
instance HasBalance Env where
getBalance = envBalance
class Monad m => MonadBalance m where
modifyBalance :: (Int -> Int) -> m ()
instance (HasBalance env, MonadIO m) => MonadBalance (ReaderT env m) where
modifyBalance f = do
env <- ask
liftIO $ atomically $ modifyTVar' (getBalance env) f
instance Monad m => MonadBalance (State.StateT Int m) where
modifyBalance = State.modify
modify :: MonadBalance m => (Int -> Int) -> m ()
modify f = do
-- Now I know there's no way I'm performing IO here
modifyBalance f
logSomething :: (MonadReader env m, HasLog env, MonadIO m)
=> String
-> m ()
logSomething msg = do
env <- ask
liftIO $ getLog env msg
main :: IO ()
main = hspec $ do
describe "modify" $ do
it "works, IO" $ do
var <- newTVarIO (1 :: Int)
runReaderT (modify (+ 2)) var
res <- readTVarIO var
res shouldBe 3
it "works, pure" $ do
let res = State.execState (modify (+ 2)) (1 :: Int)
res shouldBe 3
describe "logSomething" $ do
it "works" $ do
var <- newTVarIO ""
let logFunc msg = atomically $ modifyTVar var (++ msg)
msg1 = "Hello "
msg2 = "World\n"
runReaderT (logSomething msg1 >> logSomething msg2) logFunc
res <- readTVarIO var
res shouldBe (msg1 ++ msg2)
今,modify関数全体が型クラスの中にあるため,この短い例は馬鹿げている.しかし,より大きい例なら,ReaderTパターンを最大限に活用しながら,ロジック全体で任意の副作用が発生しないように指定できることがわかる.
別の言い方をするなら,Monadに内在するので,foo :: Monad m => Int -> m Double関数を純粋でないように見えるかもしれないが,そうではない.「Monadの任意のインスタンス」という制約を関数に与えることによって,我々は「これは実際の副作用はもたない」と述べている.結局の所,上記の型はIdentityに単一化され,もちろんこれは純粋である.
この例は少し凝っているように感じるかもしれないが,parseInt :: MonadThrow m => Text -> m Intはどうだろうか?あなたは,「それは純粋でなく,ランタイム例外を投げる」と思うかもしれない.しかしながら,この型はparseInt :: Text -> Maybe Intに単一化され,もちろんこれは純粋である.我々は関数について多くの知識を得ており,それを安全に呼べると感じられる.
要約すると,あなたの関数をmtlスタイルのMonad制約に一般化できるなら,そうすること.あなたは純粋性が持っている多くの利点を取り戻すことができる.
分析
ここでのテクニックは確かに多少手間がかかるが,大規模なアプリケーションやライブラリ開発ではそのコストは償却される.私はこのスタイルで作業することでの利点が,多くの現実のプロジェクトのコストを遥かに上回ることを見出した.
他にも問題がある.例えば,プロジェクトに参加している人にとって,エラーメッセージがもっと複雑になり,認知的なオーバーヘッドが増える.しかし,私の経験では,誰であっても,このアプローチに一旦入信すれば,うまくいく.
上記の具体的な利点に加えて,このアプローチを使用すると,現実世界で人々が経験している多くの一般的なモナド変換子スタックの問題点を回避できる.私は,他の人たちがそれらの現実の例を共有することを勧める.私はこのアプローチに固執しているので,個人的には長い間これらの問題に出会っていない.
公開後の更新
17年6月15日 ImplicitParamsについてのAshleyからの以下のコメントはその拡張に関する問題についてRedditの議論を生み出した .あなた自身で議論を読むこと.しかし私にとってのその議論でのキーポイントはMonadReaderがより良い選択であるということである.