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