一個 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)