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