我试图通过重构我第一次学习 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
我只是无法在定义中得到lift
、liftRand
和liftRandT
的任何组合。这不是特别重要,但我很好奇有效的定义可能是什么?
问题1
[编辑:这是一个问题,因为我没有使用沙盒,所以我有旧/冲突版本的库导致问题]
即使MonadRandom有所有东西的实例(MaybeT除外),我仍然必须为每个转换器编写自己的实例声明:
instance (MonadRandom m) => MonadRandom (MaybeT m) where
getRandom = lift getRandom
getRandomR = lift . getRandomR
getRandoms = lift getRandoms
getRandomRs = lift . getRandomRs
我这样做是为了WriterT
,ReaderT
,并通过从MonadRandom源代码复制实例来StateT
。注意:对于StateT
和WriterT
,它们确实使用合格的导入,但不适用于 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
对应Just
,empty
/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