如何结合数据组合和 monad 转换器

How to combine data composition and monad transformers

我对 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 上有一个潜在的 "throwing" 操作,我想在 MyState 上以有状态的抛出操作使用该操作。我显然可以通过解构整个堆栈并重建它来做到这一点,但 monadic 结构的全部意义在于我不应该这样做。是否有简洁、惯用的解决方案?

对于冗长的代码片段深表歉意——我已尽力将其缩减。

最好的解决方案是将 throwingModification 重写为 MyMonad

throwingModification :: MyMonad ()
throwingModification = do
    s <- get
    if state s then
        put $ ComposedState False
    else
        throwError $ MyError "error!"

如果你不能重写你的函数(因为它在别处使用),你可以将它包装起来。

更自然的做法是在 MyMonad monad 中从头开始写 throwingModification,如下所示:

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 的当前形式,您可能必须编写自己的组合器,因为通常的状态组合器不包括用于切换状态类型的机制 s,这就是您有效地尝试做的事情。

以下 usingState 的定义可能会有所帮助。它使用 getter 和 setter 将 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 以在一般 MonadState monad 之间工作而不是直接在 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)),然后通过combinator转化为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 . ((),))

如果你有复杂的嵌套状态,它会概括为 (composed.underneath.state4)