Monad Transformer 与 MaybeT 和 RandT 叠加

Monad Transformer stacks with MaybeT and RandT

我正在尝试通过重构我第一次学习 Haskell 时写的东西来了解 Monad Transformers 的工作原理。它有很多组件可以用(相当大的)Monad Transformers 堆栈替换。

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

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

简要说明:

在原始版本中发生了 Nothing "never" 因为我只使用 Map 来有效查找已知的 key/value 对(我想我可以重构使用数组)。在最初,我通过自由使用 fromJust.

解决了 Maybe 问题

据我所知,MaybeT 位于顶部意味着如果 Nothing 在任何 SolverT a 中,我不会丢失其他转换器中的任何信息,因为它们是从外向内展开的。

边题

[编辑:这是一个问题,因为我没有使用沙箱,所以我有 old/conflicting 个版本的库导致了问题]

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

我确实尝试为 MonadReader 写了一个实例声明,这已经是我能编译的了:

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

我无法让 liftliftRandliftRandT 的任何组合在定义中起作用。这不是特别重要,但我很好奇有效的定义是什么?

问题 1

[编辑:这是一个问题,因为我没有使用沙箱,所以我有 old/conflicting 个版本的库导致了问题]

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

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

我通过从 MonadRandom source code 复制实例来为 WriterTReaderTStateT 执行此操作。注意:对于 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

起初我以为问题出在它们的 Monad 类型上。堆栈中的所有其他 Monad 都有 (\s -> (a,s)) 的构造函数,而 Maybe 有 Just a | Nothing。但这应该没什么区别,ask 的类型应该是 return 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

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

我知道使用 lift 我可以将一些东西从我的变压器堆栈中提取到 MaybeT,这样我就可以得到 MaybeT m a。但是,如果我以 Maybe a 结束,我假设我可以将它绑定到 do 块中 <-

问题3

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

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

问题 1

无法重现。 RandT.

已经有这些实例

问题 2

lookup returns Maybe,但你有一个基于 MaybeT 的堆栈。之所以没有MonadMaybe是因为对应的类型class是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

不同之处在于失败时会发生什么。前者堆栈没有 return 任何东西,而后者允许您检索状态和 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