Monad Transformer 堆栈与 MaybeT 和 RandT 堆叠



我试图通过重构我第一次学习 Haskell 时写的东西来学习 Monad Transformers 的工作原理。它有很多组件可以用(相当大的)Monad变压器堆栈代替。

我首先为我的堆栈编写了一个类型别名:

type SolverT a = MaybeT
                    (WriterT Leaderboard
                        (ReaderT Problem
                            (StateT SolutionState
                                (Rand StdGen)))) a

简要介绍:

  • 通过用于各种随机操作的StdGen Rand线程
  • StateT在逐步评估解决方案时携带解决方案的状态
  • ReaderT具有固定状态 正在解决的问题空间
  • WriterT有一个排行榜,由解决方案不断更新,具有迄今为止的最佳版本
  • MaybeT是必需的,因为问题和解决方案状态都使用Data.Map lookup,并且配置方式中的任何错误都会导致Nothing

在原始版本中,Nothing"从未"发生,因为我只使用Map来有效查找已知的键/值对(我想我可以重构以使用数组)。在原著中,我通过自由使用fromJust来解决Maybe问题。

据我所知,将MaybeT放在顶部意味着,在任何SolverT a发生Nothing时,我都不会丢失其他变压器中的任何信息,因为它们是从外到内的。

附带问题

[编辑:这是一个问题,因为我没有使用沙盒,所以我有旧/冲突版本的库导致问题]

当我第一次写堆栈时,我RandT在顶部。我决定避免在任何地方使用 lift 或为所有其他转换器编写自己的实例声明以进行RandT。所以我把它移到了底部。

我确实尝试为MonadReader编写一个实例声明,这大约是我所能编译的:

instance (MonadReader r m,RandomGen g) => MonadReader r (RandT g m) where
    ask = undefined
    local = undefined
    reader = undefined

我只是无法在定义中得到liftliftRandliftRandT的任何组合。这不是特别重要,但我很好奇有效的定义可能是什么?

问题1

[编辑:这是一个问题,因为我没有使用沙盒,所以我有旧/冲突版本的库导致问题]

即使MonadRandom有所有东西的实例(MaybeT除外),我仍然必须为每个转换器编写自己的实例声明:

instance (MonadRandom m) => MonadRandom (MaybeT m) where
    getRandom = lift getRandom
    getRandomR = lift . getRandomR
    getRandoms = lift getRandoms
    getRandomRs = lift . getRandomRs

我这样做是为了WriterTReaderT,并通过从MonadRandom源代码复制实例来StateT。注意:对于StateTWriterT,它们确实使用合格的导入,但不适用于 Reader。如果我没有编写自己的声明,我会收到这样的错误:

No instance for (MonadRandom (ReaderT Problem (StateT SolutionState (Rand StdGen))))
  arising from a use of `getRandomR'

我不太确定为什么会发生这种情况。

问题2

有了上面的内容,我重写了我的一个函数:

randomCity :: SolverT City
randomCity = do
    cits <- asks getCities
    x <- getRandomR (0,M.size cits -1)
    --rc <- M.lookup x cits
    return undefined --rc

以上编译,我认为是应该如何使用变压器。尽管必须编写重复的转换器实例很乏味,但这非常方便。你会注意到,在上面我注释掉了两部分。如果我删除我得到的评论:

Couldn't match type `Maybe'
              with `MaybeT
                      (WriterT
                         Leaderboard
                         (ReaderT Problem (StateT SolutionState (Rand StdGen))))'
Expected type: MaybeT
                 (WriterT
                    Leaderboard (ReaderT Problem (StateT SolutionState (Rand StdGen))))
                 City
  Actual type: Maybe City

起初,我认为问题在于它们的Monads类型。堆栈中的所有其他 Monads 都有一个用于(s -> (a,s))的构造函数,而 也许 Just a | Nothing .但这应该没有区别,ask的类型应该返回Reader r a,而lookup k m应该给出一个类型Maybe a

我想我会检查我的假设,所以我进入GHCI并检查了这些类型:

> :t ask
ask :: MonadReader r m => m r
> :t (Just 5)
(Just 5) :: Num a => Maybe a
> :t MaybeT 5
MaybeT 5 :: Num (m (Maybe a)) => MaybeT m a

可以看到,我所有其他的转换器都定义了一个可以通过变压器提升的类型类。 MaybeT似乎没有MonadMaybe类型类。

我知道有了lift,我可以把变压器堆栈中的东西举到MaybeT,这样我就可以得到MaybeT m a。但是,如果我最终得到Maybe a,我认为我可以用<-将其绑定在do块中。

问题3

实际上还有一件事要添加到我的堆栈中,我不确定它应该去哪里。Solver以固定的周期数运行。我需要跟踪当前周期与最大周期。我可以将周期计数添加到解决方案状态,但我想知道是否可以添加额外的转换器。

此外,多少变压器才算太多?我知道这是非常主观的,但这些变压器肯定有性能成本吗?我想一定量的融合可以在编译时优化这一点,所以也许性能成本最小?

问题 1

无法复制。已经有这些实例用于RandT.

问题2

lookup返回Maybe,但您有一个基于 MaybeT 的堆栈。之所以没有MonadMaybe,是因为对应的类型类是MonadPlus(或更一般的Alternative)-pure/return对应Justempty/mzero对应Nothing。我建议创建一个助手

lookupA :: (Alternative f, Ord k) => k -> M.Map k v -> f v
lookupA k = maybe empty pure . M.lookup k

然后,您可以在monad堆栈中的任何需要的地方调用lookupA

正如评论中提到的,我强烈建议使用 RWST ,因为它完全适合您的情况,并且比 StateT/ReaderT/WriterT 堆栈更容易使用。

还要考虑两者之间的区别

type Solver a = RWST Problem Leaderboard SolutionState (MaybeT (Rand StdGen)) a

type Solver a = MaybeT (RWST Problem Leaderboard SolutionState (Rand StdGen)) a

区别在于发生故障时会发生什么。前者堆栈不返回任何内容,而后者允许您检索到目前为止计算的状态和Leaderboard

问题3

最简单的方法是将其添加到状态部分中。我只是把它包含在SolutionState

.

示例代码

import Control.Applicative
import Control.Monad.Random
import Control.Monad.Random.Class
import Control.Monad.Trans
import Control.Monad.Trans.Maybe
import Control.Monad.RWS
import qualified Data.Map as M
import Data.Monoid
import System.Random
-- Dummy data types to satisfy the compiler
data Problem = Problem
data Leaderboard = Leaderboard
data SolutionState = SolutionState
data City = City
instance Monoid Leaderboard where
  mempty = Leaderboard
  mappend _ _ = Leaderboard
-- dummy function
getCities :: Problem -> M.Map Int City
getCities _ = M.singleton 0 City
-- the actual sample code
type Solver a = RWST Problem Leaderboard SolutionState (MaybeT (Rand StdGen)) a
lookupA :: (Alternative f, Ord k) => k -> M.Map k v -> f v
lookupA k = maybe empty pure . M.lookup k
randomCity :: Solver City
randomCity = do
    cits <- asks getCities
    x <- getRandomR (0, M.size cits - 1)
    lookupA x cits