通过嵌套状态变换器(mtl)

时间:2017-03-01 22:11:56

标签: haskell monad-transformers lens state-monad higher-kinded-types

所以我正在开发一个可扩展的应用程序框架,该框架的一个关键部分是能够在许多不同的状态类型上运行状态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本身就是一个App,但它也会产生一个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内运行。

有什么想法吗?谢谢你的时间!!

2 个答案:

答案 0 :(得分:1)

沿着user2407038's comment,这种类型......

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 ()

...你只需要在适当的地方return进入IO ......

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 ()

答案 1 :(得分:0)

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

鉴于you have found a solution already,这个答案现在有点没有实际意义,但无论如何我觉得我应该在这里扩展我的最新评论。在其中,我曾问过:

  

你说你希望“嵌套状态的monad能够在全局状态下运行”。但是,一旦你有了它,我们真的可以将嵌套monad视为状态monads 的特定子状态吗?

我会说我们做不到。使用您建议实现的功能,也许您会为每个子状态正式拥有某种不同的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将其转到App . zoom stateA。但是,使用嵌套的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)的东西 - 因为通常它不会生成合法的镜头 - {正如我们所做的那样,{3}}才能正常运作。也就是说,通过稍微改组,我们可以用the lenses have to be disjoint来表达我们正在做的事情,尽管我不确定这是否有利于此:

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将它们提交给{{1 }。)