如何为状态类型不透明的有状态组件创建接口?

时间:2015-03-29 19:31:07

标签: haskell interface state

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

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

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

如果不清楚,则上述示例失败,因为myFunction的实现无法证明记录选择器提供了合适的类型(因为它是存在的) ;至少我是如何理解的。

2 个答案:

答案 0 :(得分:1)

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

data ProgramState cs = ProgramState { componentState :: cs }

这意味着您还必须从ComponentState公开InternalComponent.hs类型,但不是构造函数。这样您就可以为类型检查器提供一些内容,但不要向InternalComponent的用户公开任何内部。

答案 1 :(得分:1)

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

由于在嵌套状态的情况下我们需要更新更复杂对象中的值,使用lens可以使生活更轻松(另请参阅this tutorial)。类型Lens' s a的值知道如何在a内达到s类型的特定值以及如何修改它(即创建类型为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,让所有有状态计算都使用它,无论他们需要什么状态。

然而,有一个大警告:如果两个不相关的计算碰巧使用相同类型的状态,他们会分享这个值很可能是灾难性的结果!因此,对计算的不同部分的状态使用显式记录会更加安全。