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

标签 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

我在 CompositedState 上有一个潜在的“抛出”操作,并且我想在 MyState 上的有状态抛出操作中使用该操作。显然,我可以通过解构整个堆栈并重建它来做到这一点,但单子(monad)结构的重点是我不必这样做。有没有简洁、惯用的解决方案?

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

最佳答案

更自然的方法是在 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)) code>,然后由组合器转换为 s 状态,然后手动包装在 MyMonad 中,如上所述。

带镜头

我并不是说镜头是 Elixir 或者什么,但它们确实有助于清理代码中一些更丑陋的部分。

添加镜头后:

{-# 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)

关于haskell - 如何结合数据组合和 monad 转换器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46576150/

相关文章:

r - 理解 R 中的惰性求值

haskell - 如何编译依赖项以在 cabal 沙箱中进行分析

haskell - 处理 Either 和 ST monad

haskell - 使用 State Monad 插入树

Haskell - 从句柄读取行而不阻塞

linux - 与 Xmonad 一起使用时 Xmobar 不可见

haskell - 用 `catchError`写一串 `(<|>)` -s吗?

haskell - 生成状态列表的最佳方法(Haskell)

haskell - 在 Frege 中创建 State 实例

haskell - 如何创建 MonadRandom (StateT PureMT m0)? ( haskell )