如何组合数据组合和monad变换器

时间:2017-10-05 00:31:52

标签: haskell monad-transformers state-monad

我对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结构的重点在于我不应该这样做。是否有一种简洁,惯用的解决方案?

为冗长的代码段道歉 - 我尽力将其删除。

2 个答案:

答案 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!"

如果你不能重写你的功能(因为它在其他地方使用过),你可以改为包装它。