将计算从 State monad 提升到 RWS monad

Lifting a computation from the State monad into the RWS monad

我围绕 RWS (Reader+Writer+State) monad 的使用构建了一个计算:

newtype Problem a = Problem { unProblem :: RWS MyEnv MyLog MyState a }
                    deriving ({- lots of typeclasses -})

通过组装形式为

的基本计算逐步构建计算
foo :: a -> Problem b

然而,有时子计算不需要 RWS monad 的全部功能。例如,考虑

bar :: c -> State MyState d

我想在 Problem monad 的上下文中使用 bar 作为更大计算的一部分。我可以看到三种方法,none 对我来说似乎非常优雅。

  1. 手动解包 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
    
  2. 修改 bar 的类型签名,将其提升到 Problem monad 中。这有一个缺点,即新类型签名没有明确承诺 bar 独立于 MyEnv 并且不向 MyLog.

  3. 记录任何内容
  4. 用显式 ReaderT MyEnv WriterT MyLog State MyState monad 堆栈替换 RWS monad。这使我能够简洁地 lift.lift bar 子计算到完整的 monad 中;但是,这个技巧将不起作用,例如对于 c -> Reader MyEnv d.

  5. 形式的子计算

是否有更简洁的方法来组合 foobar?我有一种预感,对 type-class 实例的一些巧妙定义可能会起到作用,但我看不出具体如何进行。

我假设您正在使用 mtl(如果您没有使用,请考虑这样做 - 这些库大部分是兼容的,但以下内容除外)。您可以派生 MonadReader MyEnvMonadWriter MyLogMonadState MyState 的实例。然后,您可以使用这些在具有此类约束的 any 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 ()

现在,为什么这通常有用?它归结为你的 monad "stack" 的灵​​活性。如果明天你决定你实际上不需要 RWS 而只需要 State,你可以相应地重构 Problembar(首先只需要状态)将继续工作,没有任何变化。

一般来说,我尽量避免在foobar等辅助函数中使用WriterTStateTRWST等。选择使用类型类 MonadWriterMonadStateMonadReader 等让您的代码尽可能通用和独立于实现。然后,您只需使用 WriterTStateTRWST 一旦进入你的代码:当你实际上 "run" 你的 monad 时。

关于 transformers

的附注

如果您正在使用 transformers, none of this works. That isn't necessarily a bad thing: mtl, by virtue of always being able to "find" components (like state or writer) has some problems