【问题标题】:How to create an interface for a stateful component where the state type is opaque?如何为状态类型不透明的有状态组件创建接口?
【发布时间】:2015-03-29 19:31:07
【问题描述】:
-- InternalComponent.hs
data ComponentState = ComponentState ...
instance Default ComponentState where ...

componentFunction :: (MonadState InternalComponentState m) => a -> m a

-- Program.hs
data ProgramState = forall cs. ProgramState {
    componentState :: cs,
    ...
}

newtype MyMonad a = MyMonad { runMyMonad :: StateT ProgramState IO a }

myFunction a = do
    s <- get
    let cs = componentState s
    let (r, cs') = runState (componentFunction a) cs
    put $ s { componentState = cs' }
    return r

我想要的是能够使用MyMonad 内部的componentFunction(在myFunction 中,如示例中所示),而不对组件所需状态的实际类型特别感兴趣。将组件状态保持在我自己的状态中不是必需的,但就我在 Haskell 中使用状态的能力而言。

这实际上可以看作是在另一种编程语言中实现的有状态接口的等价物:具有某些实现的接口的实例化提供默认状态值,并且通过该接口调用的每个函数都可以修改该状态。在任何时候都不会向用户展示实现细节。

如果不清楚,上面的例子失败了,因为myFunction的实现不能证明记录选择器提供了一个合适的类型(因为它是一个存在的);至少我是这么理解的。

【问题讨论】:

  • 你的ProgramState 真的没有意义。您是说有一个名为componentState 的字段,其类型完全未知。您不能从componentState 字段神奇地“生成”ComponentState 类型的值,因为您已经对类型变量cs 进行了存在量化——cs 的类型不再对外界可见。您是正确的,“myFunction 无法证明记录选择器提供了适当的类型”。显而易见的解决方案是不量化cs - 在这种情况下,您完全不清楚为什么要这样做。
  • 这可能是相关的:Combining multiple states in StateT

标签: haskell interface state


【解决方案1】:

您可以通过组件状态的类型参数化ProgramState,例如有

data ProgramState cs = ProgramState { componentState :: cs }

这意味着您还必须从InternalComponent.hs 公开ComponentState 类型,但不是构造函数。通过这种方式,您可以为类型检查器提供一些可玩的东西,但不要向InternalComponent 的用户公开任何内部信息。

【讨论】:

  • 这就是我在库中实际使用的用户状态。我想我可以对两者进行参数化。不过,我认为我有一些问题。其中之一是每次都必须手动重复输入。
  • 当它变得太长时,您总是可以创建一个新类型,例如使用 type Program = ProgramState ComponentState 并从那里只使用 Program 而不是扩展类型。
  • 我想出了一个工作示例并将其发布到CodeReview
【解决方案2】:

首先,我建议阅读Combining multiple states in StateT,以了解其他可用选项。

由于在嵌套状态的情况下,我们需要更新更复杂对象中的值,使用lens 可以让生活变得更轻松(另请参阅this tutorial)。 Lens' s a 类型的值知道如何在s 中达到a 类型的特定值以及如何修改它(即创建一个相同的s 类型的新值,除了修改a)。然后我们可以定义一个辅助函数

runInside :: (MonadState s m) => Lens' s a -> State a r -> m r
runInside lens s = lens %%= (runState s)

给定一个镜头和a 上的状态计算,我们可以将这样的计算提升为由s 参数化的状态计算。该库允许我们使用 Template Haskell 生成镜头,例如:

{-# LANGUAGE RankNTypes, TemplateHaskell #-}
import Control.Lens.TH

data ProgramState cs = ProgramState { _componentState :: cs }

$(makeLenses ''ProgramState)

将生成componentState :: Lens' ProgramState cs(实际上生成的函数会更通用一些)。将它们组合在一起,我们得到

runInside componentState :: MonadState (ProgramState a) m => State a r -> m r

使用Typeable,我们可以更进一步,创建一个地图,自动创建或保持任何类型的状态。我一般不推荐这种方法,因为它在某种程度上避免了 Haskell 的强类型系统检查,但在某些情况下它可能有用。

{-# LANGUAGE ExistentialQuantification, ScopedTypeVariables, RankNTypes #-}
import Control.Lens
import Control.Lens.TH
import Control.Monad.State
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Typeable

data Something = forall a . Typeable a => Something a

type TypeMap = Map TypeRep Something

我们定义了一个通用的无类型容器,可以容纳 Typeable 的任何内容,以及一个将类型表示映射到它们的值的映射。

我们需要一些类来提供默认/起始值:

class Default a where
    getDefault :: a

-- just an example
instance Default Int where
    getDefault = 0

最后,我们可以创建一个给定任意Typeable 类型的镜头,它通过查找其类型表示来关注其在地图中的值:

typeLens :: forall t . (Typeable t, Default t) => Lens' TypeMap t
typeLens = lens get set
  where
    set map v = Map.insert (typeOf v) (Something v) map
    get map = case Map.lookup (typeRep (Proxy :: Proxy t)) map of
                Just (Something v) | Just r <- cast v   -> r
                _                                       -> getDefault

所以你可以在你的状态中的某个地方拥有TypeMap,让所有有状态的计算都使用它,而不管它们需要什么状态。

但是,有一个严重警告:如果两个不相关的计算碰巧使用相同的类型作为它们的状态,它们将共享该值,很可能会导致灾难性的结果!因此,对计算的不同部分的状态使用显式记录会更加安全。

【讨论】:

  • 我仍在阅读链接的答案(感谢您的评论顺便说一句!)我会尽快阅读。哦,忘了提到我故意选择不在这个库中使用 Lens(尽管我可能会后退,因为使用常规的 Haskell 记录是多么痛苦)。
  • 我想出了一个工作示例并将其发布在CodeReview
猜你喜欢
  • 2018-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-24
  • 1970-01-01
  • 2021-09-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多