Haskell——使用StateT单元变压器链接两个状态



我有两个或多个独立的状态要在一个Haskell应用程序中跟踪。

我正在使用声明两个新的类型类

type MonadTuple m = MonadState (Int, Int) m
type MonadBool m = MonadState Bool m

单变压器堆栈声明为

type Stack = StateT (Int, Int) (StateT Bool IO) ()

我打算这样使用堆栈

ret :: Stack
ret = apply
apply :: (MonadTuple m, MonadBool m) => m ()
apply = undefined

编译器不满意,因为在尝试检查Stack是否符合MonadBool时,它无法将Bool(Int, Int)匹配。

我知道在 StateT 中组合多个状态中给出的解决方案。除了箭头带镜头的全局状态之外,还有更简单的解决方案吗?

附录: 完整的代码块是

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.State.Class
import Control.Monad.State.Lazy
type MonadTuple m = MonadState (Int, Int) m
type MonadBool m = MonadState Bool m
type Stack = StateT (Int, Int) (StateT Bool IO) ()
ret :: Stack
ret = apply
apply :: (MonadTuple m, MonadBool m) => m ()
apply = undefined

>MonadState的定义有一个函数依赖m -> s,这意味着一个monadm必须最多有一个MonadState s m实例。或者,用更简洁的术语来说,同一个 monad 不能有两个不同状态的MonadState实例,这正是您要做的。

有一个更简单的解决方案:

apply :: (MonadTuple (t m), MonadBool m, MonadTrans t) => t m ()
apply = undefined

您可以使用getputapply内触摸(Int, Int)状态,使用lift getlift . put来触摸Bool状态。

但是,这要求StateT (Int, Int)成为顶级变压器。如果它低于顶部,则需要通过在类型中放置适当数量的附加变压器来对深度进行编码;例如,如果它是第三件事,那么你需要

apply :: (MonadTuple (t1 (t2 (t3 m))), MonadBool m, MonadTrans t1, MonadTrans t2, MonadTrans t3) => t1 (t2 (t3 m)) ()
apply = undefined

并且每次访问Bool状态都需要使用三个lift,这很快就会变得笨拙,并且真正失去了 MTL 风格的类多态编程的魅力。

一种常见的替代样式是公开接触两种状态但不是类多态的 API。例如

type Stack = StateT (Int, Int) (StateT Bool IO)
getTuple :: Stack (Int, Int)
getTuple = get
getBool :: Stack Bool
getBool = lift get

(同样,您将添加一个putTupleputBool

我想通过现代扩展,您还可以考虑引入自己的类,该类没有MonadState所拥有的fundep;例如

class MonadState2 s m where
get2 :: m s
put2 :: s -> m ()

然后,您可以使用 newtype 提供两个实例,以按类型消除歧义:

newtype Stack a = Stack (StateT (Int, Int) (StateT Bool IO) a)
instance MonadState2 Bool Stack where
get2 = Stack (lift get)
put2 = Stack . lift . put
instance MonadState2 (Int, Int) Stack where
get2 = Stack get
put2 = Stack . put

然后来电者会写例如get2 @Boolget2 @(Int, Int)类型推断是否没有足够的信息来选择要使用的实例。但我怀疑这会很快变老。

最新更新