我对monad变换器有点新,并且目前正在尝试在项目中使用StateT/Except
堆栈。我遇到的困难是我有几层数据组合(对它们进行操作的类型,包含在对其进行其他操作的类型中),我无法弄清楚如何优雅地使用monad变形金刚在那个设计中。具体来说,我在编写以下代码时遇到了麻烦(显然是简化示例):
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.Except
import Control.Monad.State
import Control.Monad.Trans.Except (Except, throwE)
import Control.Monad.Trans.State (StateT)
data ComposedState = ComposedState { state :: Bool }
data MyError = MyError { message :: String }
-- If the passed in state is true, change it to false; otherwise throw.
throwingModification :: ComposedState -> Except MyError ComposedState
throwingModification (ComposedState True) = return $ ComposedState False
throwingModification _ = throwE $ MyError "error!"
-- A state which composes with @ComposedState@,
data MyState = MyState { composed :: ComposedState }
-- and a monad transformer state to allow me to modify it and propagate
-- errors.
newtype MyMonad a = MyMonad { contents :: StateT MyState (Except MyError) a }
deriving ( Functor
, Applicative
, Monad
, MonadState MyState
, MonadError MyError )
anAction :: MyMonad ()
anAction = do -- want to apply throwingModification to the `composed` member,
-- propogating any exception
undefined
我有潜在的"投掷" ComposedState
上的操作,我希望在MyState
上的有状态抛出操作中使用该操作。我显然可以通过解构整个堆栈并重建它来做到这一点,但是monadic结构的重点在于我不应该这样做。是否有一种简洁,惯用的解决方案?
为冗长的代码段道歉 - 我尽力将其删除。
答案 0 :(得分:3)
更自然的做法是从throwingModification
monad开始编写MyMonad
,如下所示:
throwingModification' :: MyMonad ()
throwingModification' = do ComposedState flag <- gets composed
if not flag then throwError $ MyError "error!"
else modify (\s -> s { composed = (composed s)
{ Main.state = False } })
我在这里假设组合状态包含您要保留的其他组件,这使得modify
子句变得丑陋。使用镜头可以使它更清洁。
但是,如果您坚持使用当前形式的throwingModification
,您可能必须编写自己的组合子,因为通常的状态组合器不包含切换状态类型的机制{{1这是你有效地尝试做的事情。
s
的以下定义可能会有所帮助。它使用getter和setter将usingState
操作从一个状态转换为另一个状态。 (同样,镜头方法会更清晰。)
StateT
我认为没有一种简单的方法可以修改usingState :: (Monad m) => (s -> t) -> (s -> t -> s)
-> StateT t m a -> StateT s m a
usingState getter setter mt = do
s <- get
StateT . const $ do (a, t) <- runStateT mt (getter s)
return (a, setter s t)
在一般usingState
monad之间工作,而不是直接在MonadState
之间工作,所以你需要手动解除它通过您的StateT
数据类型进行转换。
如此定义MyMonad
,您可以编写以下内容。 (注意usingState
来自>=>
。)
Control.Monad
有助手:
MyMonad $ usingState getComposed putComposed $
StateT (throwingModification >=> return . ((),))
这仍然有点难看,但那是因为类型getComposed = composed
putComposed s c = s { composed = c }
必须适应t -> Except e t
,然后由组合器转换为StateT (t -> Except e ((), t))
状态,然后手动包装在你的s
,如上所述。
我并不是说镜片是一种神奇疗法或任何东西,但它们确实有助于清理代码中一些丑陋的部分。
添加镜片后:
MyMonad
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
import Control.Monad ((>=>))
import Control.Monad.Except (Except, MonadError, throwError)
import Control.Monad.State (get, MonadState, runStateT, StateT(..))
data MyError = MyError { _message :: String }
data MyState = MyState { _composed :: ComposedState }
data ComposedState = ComposedState { _state :: Bool }
makeLenses ''ComposedState
makeLenses ''MyError
makeLenses ''MyState
的定义看起来更清晰:
throwingModification
我上面提到的throwingModification :: ComposedState -> Except MyError ComposedState
throwingModification s =
if s^.state then return $ s&state .~ False
else throwError $ MyError "error!"
版本肯定会带来好处:
MyMonad
throwingModification' :: MyMonad ()
throwingModification' = do
flag <- use (composed.state)
if flag then composed.state .= False
else throwError (MyError "error!")
的定义看起来并没有太大不同:
usingStateL
但它允许使用现有镜头usingStateL :: (Monad m) => Lens' s t -> StateT t m a -> StateT s m a
usingStateL tPart mt = do
s <- get
StateT . const $ do (a, t) <- runStateT mt (s^.tPart)
return (a, s&tPart .~ t)
代替辅助函数:
composed
如果你有复杂的嵌套状态,它会推广到 MyMonad $ usingStateL composed $
StateT (throwingModification >=> return . ((),))
。
答案 1 :(得分:1)
最佳解决方案是将throwingModification
重写为MyMonad
。
throwingModification :: MyMonad ()
throwingModification = do
s <- get
if state s then
put $ ComposedState False
else
throwError $ MyError "error!"
如果你不能重写你的功能(因为它在其他地方使用过),你可以改为包装它。