提升嵌套状态转换器 (mtl)

Lift through nested state transformers (mtl)

所以我正在开发一个可扩展的应用程序框架,该框架的一个关键部分是能够 运行 在许多不同的状态类型上声明 monad;我已经设置好了,可以 运行 嵌套状态 monad;然而,我需要的一个关键特性是嵌套状态上的 monad 也能够对全局状态进行 运行 操作;我设法在早期的项目中使用一些复杂的 Free Monads 来组装它,但现在我正在使用 mtl 并且我有点卡住了。

这是一些背景信息:

newtype App a = App
  { runApp :: StateT AppState IO a
  } deriving (Functor, Applicative, Monad, MonadState AppState, MonadIO)

data AppState = AppState
  { _stateA :: A
  , _stateB :: B
  }
makeLenses ''AppState

我正在尝试定义如下内容:

liftApp :: MonadTrans m => App a -> m App a
liftApp = lift

由于 MonadTrans 的属性,这当然可以正常工作,但现在的技巧是当我有这样的操作时:

appAction :: App ()
appAction = ...

type ActionA a = StateT A App a
doStuffA :: ActionA ()
doStuffA = do
  thing1
  thing2
  liftApp appAction
  ...

这在我的应用程序中编译;但是当 运行 在 App 本身中出现这个时,技巧就来了:

myApp :: App ()
myApp = do
  ...
  zoomer stateA doStuffA

我写的有问题zoomer;这是一个尝试:

zoomer :: Lens' AppState s -> StateT s App r -> App r
zoomer lns act = do
  s <- get
  (r, nextState) <- runStateT (zoom lns act) s
  put nextState
  return r

问题是 runStateT (zoom lns act) s 本身是一个应用程序,但它也会产生一个 AppState,然后我需要 put 来获取更改。这意味着 <- runStateT 的 Monadic 部分引起的任何更改都会被 put nextState.

覆盖

我很确定我不应该像这样嵌套两组 MonadState AppState,但我不确定如何让它工作,因为 mtl 不允许我嵌套多个 MonadState,因为功能依赖。

我也开始尝试将其反转并让 App 成为外部变压器:

newtype App m a = App
  { runApp :: StateT AppState m a
  } deriving (Functor, Applicative, Monad, MonadState AppState, MonadIO, MonadTrans)

希望使用 MonadTrans 允许:

liftApp = lift

但是 GHC 不允许这样做:

• Expected kind ‘* -> *’, but ‘m’ has kind ‘*’
• In the second argument of ‘StateT’, namely ‘m’
  In the type ‘StateT AppState m a’
  In the definition of data constructor ‘App’

而且我不确定这是否有效...

所以这就是问题所在,我希望能够将 App monad 嵌套在 StateT 的任意级别中,不知何故 运行 在 App 中。

有什么想法吗?感谢您的宝贵时间!!

按照,这种类型...

type ActionA a = StateT A App a

...我觉得有点奇怪。如果要使用zoom stateA,那么其他状态就是AppState的一部分。假设您可以修改 A 子状态而不触及 AppState 的其余部分(否则您首先不想要 zoom),您应该能够简单地定义,例如...

doStuffA :: StateT A IO ()

... 然后将其带到 App 并使用:

zoomer :: Lens' AppState s -> StateT s IO r -> App r
zoomer l a = App (zoom l a)
GHCi> :t zoomer stateA doStuffA 
zoomer stateA doStuffA :: App ()

如果你想要一个纯粹的 doStuffA...

pureDoStuffA :: State A ()

...你只需要在适当的地方输入 returnIO...

GHCi> :t zoomer stateA (StateT $ return . runState pureDoStuffA)
zoomer stateA (StateT $ return . runState pureDoStuffA) :: App ()

... 或者,使用 mmorph 更简洁的拼写:

GHCi> :t zoomer stateA (hoist generalize pureDoStuffA)
zoomer stateA (hoist generalize pureDoStuffA) :: App ()

初步说明:我采取了非常不寻常的步骤来发布第二个答案,因为我觉得我的前一个答案已经足够独立,而这个答案从一个完全不同的角度来看这个问题。

考虑到 ,这个答案现在有点没有实际意义,但无论如何我觉得我应该在这里扩展我的最新评论。在里面,我问过:

You say you want "monads over nested states to be able to run actions over the global state as well". Once you have that, though, can we really think of the nested monads as state monads for their specific substate?

我会说我们不能。通过您建议的功能实现,也许您会为每个子状态正式拥有某种不同的 StateT 层;但是,如果您可以 运行 在这些层中进行全局状态操作,那么它们与整体状态之间的界限就会变得模糊。就隔离而言,您还不如使用单体 AppState.

但是,我可以想到对您的要求的另一种合理解释,这可能与您尝试做的事情相关,也可能不相关。也许您想为框架的组件保留不同的子状态,但有一个由所有组件共享的核心状态。从示意图上看,它可能看起来像这样:

data AppState = AppState
  { _stateA :: A
  , _stateB :: B
  }
makeLenses ''AppState

data Shared = Shared -- etc.

一种简单的布线方式是使用两个 StateT 层:

newtype App a = App
  { runApp :: StateT AppState (StateT Shared IO) a
  } deriving (Functor, Applicative, Monad, MonadState AppState, MonadIO)

现在您可以 lift 对共享状态执行操作,例如StateT A (StateT Shared IO),然后用 App . zoom stateA 把它带到 App。然而,使用嵌套的 StateT 层可能会有点尴尬。另一种方法是将 Shared 带入 AppState...

data AppState = AppState
  { _stateA :: A
  , _stateB :: B
  , _shared :: Shared
  }
makeLenses ''AppState

newtype App a = App
  { runApp :: StateT AppState IO a
  } deriving (Functor, Applicative, Monad, MonadState AppState, MonadIO)

...然后编写允许访问子状态和共享状态的镜头:

data Substate a = Substate
    { _getSubstate :: a
    , _sharedInSub :: Shared
    }
makeLenses ''Substate

-- There are, of course, lots of other ways of spelling these definitions.
subA :: Lens' AppState (Substate A)
subA = lens
    (Substate <$> _stateA <*> _shared) 
    (\app (Substate a s) -> app { _stateA = a, _shared = s })

subB :: Lens' AppState (Substate B)
subB = lens
    (Substate <$> _stateB <*> _shared) 
    (\app (Substate b s) -> app { _stateB = b, _shared = s })

现在您只需 zoom 例如subA 而不是 stateA。必须定义这些镜头有一些额外的样板,但如果需要可以减轻。

顺便说一下,在 lens 中没有组合子可以捕获这种模式——例如,类型为 Lens s t a b -> Lens s t c d -> Lens s t (a, c) (b, d) 的东西——因为通常它不会产生合法镜头 -- the lenses have to be disjoint, as ours are, for it to work properly. That said, with a little more shuffling we can express what we are doing in terms of alongside,但我不确定这是否有任何优势:

data ComponentState = ComponentState
  { _stateA :: A
  , _stateB :: B
  }
makeLenses ''AppState

newtype App a = App
  { runApp :: StateT (ComponentState, Shared) IO a
  } deriving (Functor, Applicative, Monad, MonadState AppState, MonadIO)

subA :: Lens' (ComponentState, Shared) (A, Shared)
subA = alongside stateA simple

subB :: Lens' (ComponentState, Shared) (B, Shared)
subB = alongside stateB simple

(如果你不喜欢使用裸对,你可以定义 AppStateSubstate a 与它们同构,并使用相应的 Iso 将它们提交给 alongside.)