Haskell Language
モナドトランスフォーマー
サーチ…
モナディックカウンター
モナド・トランスを使ってリーダ、ライター、ステート・モナドを構成する方法の例。ソースコードはこのリポジトリにあります
与えられた定数で値をインクリメントするカウンタを実装したい。
まず、いくつかの型と関数を定義します。
newtype Counter = MkCounter {cValue :: Int}
deriving (Show)
-- | 'inc c n' increments the counter by 'n' units.
inc :: Counter -> Int -> Counter
inc (MkCounter c) n = MkCounter (c + n)
カウンタを使用して次の計算を実行するとします。
- カウンタを0に設定する
- インクリメント定数を3に設定する
- カウンターを3回増分する
- インクリメント定数を5に設定する
- カウンターを2回インクリメントする
状態モナドは状態を渡すための抽象化を提供します。状態モナドを利用して、増分関数を状態変換器として定義することができます。
-- | CounterS is a monad.
type CounterS = State Counter
-- | Increment the counter by 'n' units.
incS :: Int-> CounterS ()
incS n = modify (\c -> inc c n)
これにより、より明確かつ簡潔な方法で計算を表現することができます。
-- | The computation we want to run, with the state monad.
mComputationS :: CounterS ()
mComputationS = do
incS 3
incS 3
incS 3
incS 5
incS 5
しかし、それぞれの呼び出しでインクリメント定数を渡す必要があります。私たちはこれを避けたいと思います。
環境の追加
リーダモナドは、周囲に環境を渡す便利な方法を提供します。このモナドは、OO世界で何が依存関係注入として知られているかを実行する関数型プログラミングで使用されます。
最も簡単なバージョンでは、リーダモナドには2つのタイプが必要です。
読み込まれる値のタイプ(つまり、環境、
r
以下)リーダーモナド(によって返される値以下)。
a
読者ra
しかし、状態モナドも利用する必要があります。したがって、 ReaderT
トランスを使用する必要があります。
newtype ReaderT r m a :: * -> (* -> *) -> * -> *
ReaderT
を使用して、次のように環境と状態でカウンタを定義できます。
type CounterRS = ReaderT Int CounterS
環境からのインクリメント定数を( ask
を使用して)取るincR
関数を定義し、 lift
関数( モナド変圧器クラスに属する)を使用するCounterS
モナドの増分関数を定義します。
-- | Increment the counter by the amount of units specified by the environment.
incR :: CounterRS ()
incR = ask >>= lift . incS
リーダモナドを使用して、以下のように計算を定義することができます。
-- | The computation we want to run, using reader and state monads.
mComputationRS :: CounterRS ()
mComputationRS = do
local (const 3) $ do
incR
incR
incR
local (const 5) $ do
incR
incR
要件が変更されました:ログが必要です!
ここで、我々の計算にロギングを追加して、時間内にカウンタの進化を見ることができると仮定します。
この作業を行うためのモナドである作家のモナドもあります。読者のモナドと同様に、それらを合成するので、読者のモナドトランスを使用する必要があります。
newtype WriterT w m a :: * -> (* -> *) -> * -> *
ここでw
(私たちは、この値を蓄積することを可能にするモノイドをなければならない)蓄積する出力のタイプを表し、 m
内部モナドであり、計算の種類。 a
次に、ロギング、環境、および状態を使用して、次のようにカウンタを定義できます。
type CounterWRS = WriterT [Int] CounterRS
また、 lift
を利用することで、各インクリメント後にカウンタの値を記録するインクリメント関数のバージョンを定義することができます。
incW :: CounterWRS ()
incW = lift incR >> get >>= tell . (:[]) . cValue
今度は、ロギングを含む計算を次のように書くことができます。
mComputationWRS :: CounterWRS ()
mComputationWRS = do
local (const 3) $ do
incW
incW
incW
local (const 5) $ do
incW
incW
一度にすべてを行う
この例は、作業中のモナド変圧器を示すことを意図しています。しかし、すべてのアスペクト(環境、状態、ロギング)を1回のインクリメント操作で合成することで同じ効果を得ることができます。
これを行うには、型制約を利用します。
inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue
ここでは、上記の制約を満たすモナドのために働くソリューションに到達します。計算関数は以下のように定義されます:
mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
体の中で私たちはincを利用しているからです。
たとえば、 ghci
REPLで次のようにこの計算を実行できます。
runState ( runReaderT ( runWriterT mComputation' ) 15 ) (MkCounter 0)