一個 monadic 計數器
關於如何使用 monad 變換器組合 reader,writer 和 state monad 的示例。原始碼可以在此儲存庫中找到
我們想要實現一個計數器,它將其值增加一個給定的常量。
我們首先定義一些型別和函式:
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 次
該狀態單子為圍繞穿過狀態的抽象。我們可以使用狀態 monad,並將增量函式定義為狀態變換器。
-- | 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
但是我們仍然必須在每次呼叫時傳遞增量常量。我們想避免這種情況。
新增環境
該讀者單子提供了一個方便的方式來傳遞周圍的環境。這個 monad 用於函數語言程式設計,以執行 OO 世界中稱為依賴注入的內容。
在最簡單的版本中,閱讀器 monad 需要兩種型別:
-
正在讀取的值的型別(即我們的環境,下面的
r
), -
讀者 monad 返回的值(下面的
a
)。讀者 ra
但是,我們也需要使用狀態 monad。因此,我們需要使用 ReaderT
變壓器:
newtype ReaderT r m a :: * -> (* -> *) -> * -> *
使用 ReaderT
,我們可以用環境和狀態定義我們的計數器,如下所示:
type CounterRS = ReaderT Int CounterS
我們定義了一個 incR
函式,它從環境中獲取增量常量(使用 ask
),並根據 CounterS
monad 定義我們的增量函式,我們使用 lift
函式(屬於 monad 變換器類)。
-- | Increment the counter by the amount of units specified by the environment.
incR::CounterRS ()
incR = ask >>= lift . incS
使用 reader monad,我們可以定義我們的計算如下:
-- | 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
要求改變了:我們需要記錄!
現在假設我們想要將記錄新增到我們的計算中,以便我們能夠及時看到計數器的演變。
我們還有一個 monad 來完成這項任務,作家 monad 。與閱讀器 monad 一樣,由於我們正在編寫它們,我們需要使用閱讀器 monad 變換器:
newtype WriterT w m a :: * -> (* -> *) -> * -> *
這裡 w
表示要累積的輸出的型別(必須是 monoid,這允許我們累積這個值),m
是內部 monad,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
一氣呵成
這個例子旨在顯示 monad 變換器在工作。但是,我們可以通過在單個增量操作中組合所有方面(環境,狀態和日誌記錄)來實現相同的效果。
為此,我們使用型別約束:
inc' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
inc' = ask >>= modify . (flip inc) >> get >>= tell . (:[]) . cValue
在這裡,我們得出一個解決方案,適用於滿足上述約束的任何 monad。因此,計算函式的型別定義為:
mComputation' :: (MonadReader Int m, MonadState Counter m, MonadWriter [Int] m) => m ()
因為在它的身體裡我們使用 inc’。
我們可以在 ghci
REPL 中執行這個計算,如下所示:
runState ( runReaderT ( runWriterT mComputation' ) 15 ) (MkCounter 0)