在StateT中组合多个状态

时间:2012-12-17 14:22:05

标签: haskell state

我正在编写一个作为守护进程运行的程序。 要创建守护程序,用户提供一组 每个必需类的实现(其中一个是数据库) 所有这些类都有功能 类型StateT s IO a形式的签名, 但s对每个班级都不同。

假设每个类都遵循这种模式:

import Control.Monad (liftM)
import Control.Monad.State (StateT(..), get)

class Hammer h where
  driveNail :: StateT h IO ()

data ClawHammer = MkClawHammer Int -- the real implementation is more complex

instance Hammer ClawHammer where
  driveNail = return () -- the real implementation is more complex

-- Plus additional classes for wrenches, screwdrivers, etc.

现在我可以定义一个代表所选实现的记录 每个“插槽”的用户。

data MultiTool h = MultiTool {
    hammer :: h
    -- Plus additional fields for wrenches, screwdrivers, etc.
  }

守护进程在StateT (MultiTool h ...) IO ()完成大部分工作 单子。

现在,由于多功能工具包含锤子,我可以在任何情况下使用它 需要锤子的地方。换句话说,MultiTool类型 如果我编写这样的代码,可以实现它包含的任何类:

stateMap :: Monad m => (s -> t) -> (t -> s) -> StateT s m a -> StateT t m a
stateMap f g (StateT h) = StateT $ liftM (fmap f) . h . g

withHammer :: StateT h IO () -> StateT (MultiTool h) IO ()
withHammer runProgram = do
  t <- get
  stateMap (\h -> t {hammer=h}) hammer runProgram

instance Hammer h => Hammer (MultiTool h) where
  driveNail = withHammer driveNail

withHammerwithWrenchwithScrewdriver等的实施方式 基本相同。能写一些东西真好 像这样...

--withMember accessor runProgram = do
--  u <- get
--  stateMap (\h -> u {accessor=h}) accessor runProgram

-- instance Hammer h => Hammer (MultiTool h) where
--   driveNail = withMember hammer driveNail

但当然不会编译。

我怀疑我的解决方案过于面向对象。 有没有更好的办法? 可能是Monad变形金刚? 提前感谢您的任何建议。

4 个答案:

答案 0 :(得分:28)

如果你想像你的情况一样处于一个大的全球状态,那么你想要使用的是镜头,正如Ben所建议的那样。我也推荐Edward Kmett的镜头库。然而,还有另一种可能更好的方式。

服务器具有程序连续运行并在状态空间上执行相同操作的属性。当您想要模块化服务器时,麻烦就开始了,在这种情况下,您不仅需要一些全局状态。您希望模块具有自己的状态。

让我们将模块视为将请求转换为响应的内容:

Module :: (Request -> m Response) -> Module m

现在,如果它有某种状态,那么这种状态变得明显,因为模块下次可能会给出不同的答案。有很多方法可以做到这一点,例如:

Module :: s -> ((Request, s) -> m (Response s)) -> Module m

但表达这个的更好,更等效的方法是以下构造函数(我们将尽快构建一个类型):

Module :: (Request -> m (Response, Module m)) -> Module m

此模块将请求映射到响应,但沿途还会返回自身的新版本。让我们更进一步,提出多态的请求和响应:

Module :: (a -> m (b, Module m a b)) -> Module m a b

现在,如果模块的输出类型与另一个模块的输入类型匹配,那么您可以像常规函数一样组合它们。该组合物是关联的并且具有多态性身份。这听起来很像一个类别,事实上它是!它是一个类别,一个应用函子和一个箭头。

newtype Module m a b =
    Module (a -> m (b, Module m a b))

instance (Monad m) => Applicative (Module m a)
instance (Monad m) => Arrow (Module m)
instance (Monad m) => Category (Module m)
instance (Monad m) => Functor (Module m a)

我们现在可以编写两个具有各自本地状态的模块,甚至不知道它!但这还不够。我们想要更多。可以切换的模块怎么样?让我们扩展我们的小模块系统,使模块实际上可以选择 not 来给出答案:

newtype Module m a b =
    Module (a -> m (Maybe b, Module m a b))

这允许与(.)正交的另一种形式的合成:现在我们的类型也是Alternative个仿函数的一族:

instance (Monad m) => Alternative (Module m a)

现在模块可以选择是否响应请求,如果没有,则尝试下一个模块。简单。你刚刚彻底改造了电线类别。 =)

当然,你不需要重新发明这一点。 Netwire库实现了这种设计模式,并附带了一个由预定义的&#34;模块组成的大型库。 (称为电线)。有关教程,请参阅Control.Wire模块。

答案 1 :(得分:17)

这是一个如何像其他人一样使用lens的具体例子。在下面的代码示例中,Type1是本地状态(即您的锤子),Type2是全局状态(即您的多功能工具)。 lens提供zoom函数,可让您运行本地化状态计算,放大镜头定义的任何字段:

import Control.Lens
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State

data Type1 = Type1 {
    _field1 :: Int   ,
    _field2 :: Double}

field1 :: SimpleLens Type1 Int
field1 = lens _field1 (\x a -> x { _field1 = a})

field2 :: SimpleLens Type1 Double
field2 = lens _field2 (\x a -> x { _field2 = a})

data Type2 = Type2 {
    _type1  :: Type1 ,
    _field3 :: String}

type1 :: SimpleLens Type2 Type1
type1 = lens _type1 (\x a -> x { _type1 = a})

field3 :: SimpleLens Type2 String
field3 = lens _field3 (\x a -> x { _field3 = a})

localCode :: StateT Type1 IO ()
localCode = do
    field1 += 3
    field2 .= 5.0
    lift $ putStrLn "Done!"

globalCode :: StateT Type2 IO ()
globalCode = do
    f1 <- zoom type1 $ do
        localCode
        use field1
    field3 %= (++ show f1)
    f3 <- use field3
    lift $ putStrLn f3

main = runStateT globalCode (Type2 (Type1 9 4.0) "Hello: ")

zoom不仅限于某种类型的直接子字段。由于镜头是可组合的,因此只需执行以下操作即可在一次操作中进行缩放:

zoom (field1a . field2c . field3b . field4j) $ do ...

答案 2 :(得分:6)

这听起来非常像镜头的应用。

镜头是某些数据的子字段的规范。我们认为您有一些价值toolLens和功能view以及set,以便view toolLens :: MultiTool h -> h获取工具,set toolLens :: MultiTool h -> h -> MultiTool h将其替换为新值。然后,您可以轻松地将withMember定义为仅接受镜头的功能。

镜头技术最近取得了很大的进步,现在它们的能力非常强大。在撰写本文时,最强大的库是Edward Kmett的lens库,它有点难以接受,但是一旦找到所需的功能就会非常简单。您还可以在此处搜索有关镜头的更多问题,例如: Functional lenses链接到lenses, fclabels, data-accessor - which library for structure access and mutation is betterlenses标记。

答案 3 :(得分:1)

我创建了一个名为data-diverse-lens的透明可扩展记录库,它允许组合多个ReaderT(或StateT),如gist

 {-# LANGUAGE FlexibleContexts #-}

 -- in some library code
 ...
 logInAnyReaderHasLogger :: (Has Logger r, MonadReader r m) => LogString -> m ()
 logInAnyReaderHasLogger s = asks getter >>= logWithLogger s

 queryInAnyReaderHasSQL :: (Has SqlBackEnd r, MonadReader r m) => Query -> m a
 queryInAnyReaderHasSQL q = asks getter >>= queryWithSQL q
 ...

 -- now you want to use these effects together
 ...
 logger <- initLogger  ...
 sql <- initSqlBackEnd ...

 (`runReader` (logger, sql)) $ do
       ...
       logInAnyReaderHasLogger ...
       ...
       x <- queryInAnyReaderHasSQL ...
       ...  

Data.Has是一个更简单的库,它与元组一样。库首页的示例:

ClinicDoctorSchedule