我围绕RWS
(Reader
+ Writer
+ State
)monad的使用来构建计算:
newtype Problem a = Problem { unProblem :: RWS MyEnv MyLog MyState a }
deriving ({- lots of typeclasses -})
通过组装形式的基本计算逐步建立计算
foo :: a -> Problem b
但是,有时子计算不需要RWS
单子的全部功能。例如,考虑
bar :: c -> State MyState d
我想bar
在Problem
monad的上下文中用作较大计算的一部分。我可以看到执行此操作的三种方法,但对我而言,这三种方法都不是很优雅。
手动解包State
计算并将其重新打包到RWS monad中:
baz :: a -> RWS MyEnv MyLog MyState c
baz x = do temp <- foo x
initialState <- get
let (finalResult, finalState) = runState (bar temp) initialState
put finalState
return finalResult
bar
通过将它的类型签名提升到Problem
monad中来进行修改。不利之处在于,新类型签名没有明确承诺bar
独立于且不MyEnv
记录任何内容MyLog
。
用RWS
显式ReaderT MyEnv WriterT MyLog State MyState
monad堆栈替换monad。这使我可以lift.lift
将子计算bar
简化为完整的monad。但是,此技巧不适用于例如形式为的子计算c -> Reader MyEnv d
。
是否有一个更清洁的方式来撰写foo
和bar
?我有种预感,一些聪明的类型类实例定义可能会解决这个问题,但是我无法确切知道如何进行。
我假设您正在使用mtl
(如果不使用,请考虑这样做-这些库几乎兼容,除了以下内容)。你可以得到的情况下MonadReader MyEnv
,MonadWriter MyLog
和MonadState MyState
。然后,您可以使用它们在具有此类约束的任何monad堆栈上泛化函数。
{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses, FlexibleContexts #-}
import Control.Monad.RWS
import Control.Monad.Reader
import Control.Monad.Writer
import Control.Monad.State
newtype Problem a = Problem { unProblem :: RWS MyEnv MyLog MyState a }
deriving (Functor, Applicative, Monad,
MonadReader MyEnv, MonadWriter MyLog, MonadState MyState)
从您的示例中,也许bar
只需要知道存在某种状态MyState
,就可以给它签名
bar :: MonadState MyState m => c -> m d
bar = ...
然后,即使对于foo
可能需要全部RWS
功能的,您也可以编写
foo :: (MonadState MyState m, MonadReader MyEnv m, MonadWriter MyLog m) => a -> m b
foo = ...
然后,您可以根据自己的喜好混合并匹配它们:
baz :: Problem ()
baz = foo 2 >> bar "hi" >> return ()
现在,为什么这通常有用?它归结为单子“堆栈”的灵活性。如果明天您决定实际上并不需要,RWS
而只需State
,则可以进行相应的重构Problem
,并且bar
(首先是仅需要的状态)将继续工作,而无需进行任何更改。
一般情况下,我尽量避免使用WriterT
,StateT
,RWST
像等内部辅助功能foo
或bar
。选择在那里让你的代码的通用和使用类型类实现独立的地MonadWriter
,MonadState
,MonadReader
,等。然后,你只需要使用WriterT
,StateT
,RWST
一旦你的代码中:当你真正“跑”的单子。
transformers
如果您使用transformers
,那么这些都无效。这不一定是一件坏事:mtl
由于始终能够“查找”组件(例如state或writer)存在一些问题。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句