我写的是作为后台进程运行的程序。 为了创建守护程序,用户提供了一组实施方案中的每一个所需的类(它们中的一个是一个数据库)所有这些类的具有函数具有以下形式的式签名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
但是的实现withHammer
, withWrench
, withScrewdriver
等基本上都是相同的。 这将是很好能够写出这样的事情...
--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的变压器,也许? 预先感谢您的任何建议。
如果你想要去一个大的全局状态就像在你的情况,那么你要使用什么是镜头,由Ben的建议。 我也建议爱德华Kmett的镜头库。 然而,还有另外一个,也许是更好的方式。
服务器具有程序连续运行,并在状态空间执行相同的操作属性。 麻烦的开始,当你想模块化服务器,在这种情况下,你需要的不仅仅是一些全球性状态的更多。 你想要的模块有自己的状态。
让我们考虑一个模块的东西,其转换一个请求的响应 :
Module :: (Request -> m Response) -> Module m
现在,如果它有一些国家,那么这个国家成为该模块可能会给出不同的答案,下一次noticable。 有许多方法可以做到这一点,例如以下内容:
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)
现在,我们可以组成具有自己独立的本地状态,甚至不知道它的两个模块! 但是,这还不够。 我们想要更多。 怎么样能之间进行切换模块? 让我们扩展我们的小模块系统,使得模块实际上可以选择不给出答案:
newtype Module m a b =
Module (a -> m (Maybe b, Module m a b))
这允许的成分另一种形式的正交(.)
我们现在还型是一个家庭Alternative
仿函数:
instance (Monad m) => Alternative (Module m a)
现在,一个模块可以选择是否响应请求,如果没有,下一个模块将受到审判。 简单。 你刚才又彻底改造了电线类。 =)
当然,你并不需要彻底改造这个。 所述Netwire库实现这样的设计图案,并配备了预定义的“模块”(称为线)大的库。 见Control.Wire的教程模块。
这里有一个如何使用一个具体的例子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 ...
这听起来非常像镜头的应用程序。
镜片是一些数据的一个子场的的规范。 我们的想法是,你有一定的价值toolLens
和功能view
和set
,使view toolLens :: MultiTool h -> h
获取工具和set toolLens :: MultiTool h -> h -> MultiTool h
用一个新值替换它。 然后你就可以很容易地定义你的withMember
的功能只是接受镜头。
镜头技术最近提出了一个伟大的交易,他们现在令人难以置信的能力。 在写作的时候身边最有力的库是爱德华Kmett的lens
库中,这是一个有点很难接受,但相当简单,一旦你找到你想要的功能。 您也可以搜索有关的镜头在这里让更多的问题,比如功能的镜头可链接到镜头,fclabels,数据访问-这库结构访问和突变是更好的 ,或者镜头标记。
我创建称为带透镜的可扩展记录库数据多样透镜 ,其允许组合多个ReaderT(或StateT)这样要旨 :
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Control.Lens
import Control.Monad.Reader
import Control.Monad.State
import Data.Diverse.Lens
import Data.Semigroup
foo :: (MonadReader r m, HasItem' Int r, HasItem' String r) => m (Int, String)
foo = do
i <- view (item' @Int) -- explicitly specify type
s <- view item' -- type can also be inferred
pure (i + 10, s <> "bar")
bar :: (MonadState s m, HasItem' Int s, HasItem' String s) => m ()
bar = do
(item' @Int) %= (+10) -- explicitly specify type
item' %= (<> "bar") -- type can also be inferred
pure ()
main :: IO ()
main = do
-- example of running ReaderT with multiple items
(i, s) <- runReaderT foo ((2 :: Int) ./ "foo" ./ nil)
putStrLn $ show i <> s -- prints out "12foobar"
-- example of running StateT with multiple items
is <- execStateT bar ((2 :: Int) ./ "foo" ./ nil)
putStrLn $ show (view (item @Int) is) <> (view (item @String) is) -- prints out "12foobar"
Data.Has是做同样的元组用一个简单的库。 从库中头版例如:
{-# 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 ...
...