我有一点建筑问题,我想知道是否有一个可以帮助我的常见模式或抽象。我是writing a game engine,用户可以将游戏循环指定为表单的monadic计算:
gameLoop :: TimeStep -> a -> Game a
其中Game
monad有一堆接入点,用于绘制,转换和连接引擎。然后,我还提供了一个用户调用来运行模拟的函数
runGame :: (TimeStep -> a -> Game a) -> a -> IO a
该库的主要设计目标之一是不使Game
成为MonadIO
类型类的实例。这是为了防止用户通过改变底层图形调用的状态或在不期望的情况下加载内容来自己拍脚。但是,在游戏循环已经开始之后,通常会出现IO a
的结果有用的用例。特别是,会想到用程序生成的图形元素产生敌人。
因此,我希望允许用户使用类似于以下界面的内容来请求资源:
data ResourceRequestResult a
= NotLoaded
| Loaded a
newtype ResourceRequest a = ResourceRequest {
getRequestResult :: Game (ResourceRequestResult a)
}
requestResource :: IO a -> Game (ResourceRequest a)
有了这个,我想分叉一个线程来加载资源并将结果传递给Game
monad的上下文并返回给用户。主要目标是我决定何时发生IO动作 - 我希望它在某个地方,而不是在游戏循环的中间。
我想到的一个想法是在Game
monad之上放置另一个用户定义的monad变换器...类似
newtype ResourceT r m a = ResourceT (StateT [ResourceRequest r] m a)
但是,我认为然后根据f :: ResourceT r Game a
指定事物会成为API的噩梦,因为我必须支持任何可能的monad变换器堆栈组合。理想情况下,我也希望避免在Game
中使r
多态,因为它会增加基础Game
函数的详细程度和可移植性。
Haskell是否有类似这种编程模式的抽象或习语?是我想要的不可能吗?
答案 0 :(得分:7)
最简单的方法是使用模块级封装。像这样:
module Game (Game, loadResource) where
data GameState -- = ...
newtype Game = Game { runGame :: StateT GameState IO a }
io :: IO a -> Game a
io = Game . liftIO
loadResource :: IO a -> Game (Game a)
loadResource action = io $ do
v <- newEmptyMVar
forkIO (action >>= putMVar v)
return . io $ takeMVar v
如此处所示,您可以使用Game
可以在IO
模块中执行Game
这一事实,而不会将此事实暴露给世界其他地方,只暴露{ {1}}您认为“安全”。特别是,您不会使IO
成为Game
的实例(并且它不能成为MonadIO
的实例,因为它的类型错误)。此外,MonadTrans
函数和io
构造函数不会导出,因此用户无法以这种方式进行结束运行。
答案 1 :(得分:7)
Monads,尤其是monad变形金刚come from trying to build complicated programs out of simpler pieces。新职责的另一个变换器是在Haskell中处理这个问题的惯用方法。
处理变压器堆栈的方法不止一种。由于您已在代码中使用mtl,因此我假设您可以选择使用类型来穿透变换器堆栈。
下面给出的例子对玩具问题完全过分。这整个例子非常庞大 - 它展示了如何通过多种不同方式定义的monad组合 - 就IO而言,就像RWST
这样的变换器而言,以及来自仿函数的免费monad。
我喜欢完整的示例,因此我们将从游戏引擎的完整界面开始。这将是一个小类集合,每个类型代表游戏引擎的一个责任。最终目标是提供具有以下类型的功能
{-# LANGUAGE RankNTypes #-}
runGame :: (forall m. MonadGame m => m a) -> IO a
只要MonadGame
不包含MonadIO
,runGame
的用户通常就无法使用IO
。我们仍然可以导出所有基础类型并编写像MonadIO
这样的实例,只要他们通过runGame
进入库,他们仍然可以确保他们没有犯错。 。这里提出的类型类实际上是same as a free monad, and you don't have to choose between them。
如果您因某种原因不喜欢排名2类型或免费monad,则可以改为创建一个没有MonadIO
实例的新类型,而不是导出{{3}中的构造函数}。
我们的界面将包含四个类型类 - 用于处理状态的MonadGameState
,用于处理资源的MonadGameResource
,用于绘制的MonadGameDraw
以及包含所有类型的总体MonadGame
其他三个是为了方便。
MonadGameState
是来自MonadRWS
的{{1}}的更简单版本。定义我们自己的类的唯一原因是Control.Monad.RWS.Class
仍可供其他人使用。 MonadRWS
需要游戏配置的数据类型,如何输出要绘制的数据以及维护状态。
MonadGameState
通过返回一个操作来处理资源,该操作可以在以后运行以获取资源(如果已加载)。
import Data.Monoid
data GameConfig = GameConfig
newtype GameOutput = GameOutput (String -> String)
instance Monoid GameOutput where
mempty = GameOutput id
mappend (GameOutput a) (GameOutput b) = GameOutput (a . b)
data GameState = GameState {keys :: Maybe String}
class Monad m => MonadGameState m where
getConfig :: m GameConfig
output :: GameOutput -> m ()
getState :: m GameState
updateState :: (GameState -> (a, GameState)) -> m a
我将为游戏引擎添加另一个问题,并消除对class (Monad m) => MonadGameResource m where
requestResource :: IO a -> m (m (Maybe a))
的需求。而不是通过返回值绘制,我的界面将通过明确要求它来绘制。 (TimeStep -> a -> Game a)
的返回会告诉我们draw
。
TimeStep
最后,data TimeStep = TimeStep
class Monad m => MonadGameDraw m where
draw :: m TimeStep
将需要其他三个类型类的实例。
MonadGame
为Daniel Wagner's answer提供所有四种类型类的默认定义很容易。我们将class (MonadGameState m, MonadGameDraw m, MonadGameResource m) => MonadGame m
添加到所有三个类中。
default
我知道我计划将{-# LANGUAGE DefaultSignatures #-}
class Monad m => MonadGameState m where
getConfig :: m GameConfig
output :: GameOutput -> m ()
getState :: m GameState
updateState :: (GameState -> (a, GameState)) -> m a
default getConfig :: (MonadTrans t, MonadGameState m) => t m GameConfig
getConfig = lift getConfig
default output :: (MonadTrans t, MonadGameState m) => GameOutput -> t m ()
output = lift . output
default getState :: (MonadTrans t, MonadGameState m) => t m GameState
getState = lift getState
default updateState :: (MonadTrans t, MonadGameState m) => (GameState -> (a, GameState)) -> t m a
updateState = lift . updateState
class (Monad m) => MonadGameResource m where
requestResource :: IO a -> m (m (Maybe a))
default requestResource :: (Monad m, MonadTrans t, MonadGameResource m) => IO a -> t m (t m (Maybe a))
requestResource = lift . liftM lift . requestResource
class Monad m => MonadGameDraw m where
draw :: m TimeStep
default draw :: (MonadTrans t, MonadGameDraw m) => t m TimeStep
draw = lift draw
用于州,RWST
用于资源,IdentityT
用于绘制,因此我们现在为所有这些变换器提供实例。
FreeT
我们计划从import Control.Monad.RWS.Lazy
import Control.Monad.Trans.Free
import Control.Monad.Trans.Identity
instance (Monoid w, MonadGameState m) => MonadGameState (RWST r w s m)
instance (Monoid w, MonadGameDraw m) => MonadGameDraw (RWST r w s m)
instance (Monoid w, MonadGameResource m) => MonadGameResource (RWST r w s m)
instance (Monoid w, MonadGame m) => MonadGame (RWST r w s m)
instance (Functor f, MonadGameState m) => MonadGameState (FreeT f m)
instance (Functor f, MonadGameDraw m) => MonadGameDraw (FreeT f m)
instance (Functor f, MonadGameResource m) => MonadGameResource (FreeT f m)
instance (Functor f, MonadGame m) => MonadGame (FreeT f m)
instance (MonadGameState m) => MonadGameState (IdentityT m)
instance (MonadGameDraw m) => MonadGameDraw (IdentityT m)
instance (MonadGameResource m) => MonadGameResource (IdentityT m)
instance (MonadGame m) => MonadGame (IdentityT m)
构建游戏状态,因此我们会为RWST
GameT
newtype
RWST
。这允许我们附加我们自己的实例,如MonadGameState
。我们可以使用GeneralizedNewtypeDeriving
派生尽可能多的课程。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- Monad typeclasses from base
import Control.Applicative
import Control.Monad
import Control.Monad.Fix
-- Monad typeclasses from transformers
import Control.Monad.Trans.Class
import Control.Monad.IO.Class
-- Monad typeclasses from mtl
import Control.Monad.Error.Class
import Control.Monad.Cont.Class
newtype GameT m a = GameT {getGameT :: RWST GameConfig GameOutput GameState m a}
deriving (Alternative, Monad, Functor, MonadFix, MonadPlus, Applicative,
MonadTrans, MonadIO,
MonadError e, MonadCont,
MonadGameDraw)
我们还会为MonadGameResource
提供无法控制的实例,并提供相当于runRWST
的便利功能
instance (MonadGameResource m) => MonadGameResource (GameT m)
runGameT :: GameT m a -> GameConfig -> GameState -> m (a, GameState, GameOutput)
runGameT = runRWST . getGameT
这让我们能够提供MonadGameState
,只需将所有内容传递到RWST
。
instance (Monad m) => MonadGameState (GameT m) where
getConfig = GameT ask
output = GameT . tell
getState = GameT get
updateState = GameT . state
如果我们刚刚将MonadGameState
添加到已经提供资源和绘图支持的内容中,我们只创建了MonadGame
。
instance (MonadGameDraw m, MonadGameResource m) => MonadGame (GameT m)
我们可以使用monad transformers中的IO
和MVar
来处理资源。我们创建一个变换器,所以我们有一个类型来附加MonadGameResource
的实例。这完全是矫枉过正。要将矫枉过正添加到矫枉过正,我只需要newType
IdentityT
来获取其MonadTrans
实例。我们将尽我们所能。
newtype GameResourceT m a = GameResourceT {getGameResourceT :: IdentityT m a}
deriving (Alternative, Monad, Functor, MonadFix, Applicative,
MonadTrans, MonadIO,
MonadError e, MonadReader r, MonadState s, MonadWriter w, MonadCont,
MonadGameState, MonadGameDraw)
runGameResourceT :: GameResourceT m a -> m a
runGameResourceT = runIdentityT . getGameResourceT
我们将为MonadGameResource
添加一个实例。这与其他答案完全相同。
gameResourceIO :: (MonadIO m) => IO a -> GameResourceT m a
gameResourceIO = GameResourceT . IdentityT . liftIO
instance (MonadIO m) => MonadGameResource (GameResourceT m) where
requestResource a = gameResourceIO $ do
var <- newEmptyMVar
forkIO (a >>= putMVar var)
return (gameResourceIO . tryTakeMVar $ var)
如果我们只是将资源处理添加到已经支持绘图和状态的内容中,我们就会有MonadGame
instance (MonadGameState m, MonadGameDraw m, MonadIO m) => MonadGame (GameResourceT m)
像加布里埃尔·冈萨雷斯指出的那样,&#34;你可以jcast's answer&#34;。我们将使用此技巧来实现MonadGameDraw
。唯一的绘图操作是使用Draw
到下一步操作的函数TimeStep
。
newtype DrawF next = Draw (TimeStep -> next)
deriving (Functor)
结合免费的monad变换器,这是我用来消除对(TimeStep -> a -> Game a)
的需要的技巧。我们的DrawT
转换器会将绘图责任添加到FreeT DrawF
的monad。
newtype DrawT m a = DrawT {getDrawT :: FreeT DrawF m a}
deriving (Alternative, Monad, Functor, MonadPlus, Applicative,
MonadTrans, MonadIO,
MonadError e, MonadReader r, MonadState s, MonadWriter w, MonadCont,
MonadFree DrawF,
MonadGameState)
我们再一次为MonadGameResource
和另一个便利功能定义默认实例。
instance (MonadGameResource m) => MonadGameResource (DrawT m)
runDrawT :: DrawT m a -> m (FreeF DrawF a (FreeT DrawF m a))
runDrawT = runFreeT . getDrawT
MonadGameDraw
实例说我们需要Free (Draw next)
next
要做的事情是return
TimeStamp
。
instance (Monad m) => MonadGameDraw (DrawT m) where
draw = DrawT . FreeT . return . Free . Draw $ return
如果我们只是将绘图添加到已处理状态和资源的内容中,我们就会有MonadGame
instance (MonadGameState m, MonadGameResource m) => MonadGame (DrawT m)
绘图和游戏状态相互影响 - 当我们绘制时,我们需要从RWST
获取输出以了解要绘制的内容。如果GameT
直接位于DrawT
下,则很容易做到这一点。我们的玩具循环非常简单;它绘制输出并从输入中读取行。
runDrawIO :: (MonadIO m) => GameConfig -> GameState -> DrawT (GameT m) a -> m a
runDrawIO cfg s x = do
(f, s, GameOutput w) <- runGameT (runDrawT x) cfg s
case f of
Pure a -> return a
Free (Draw f) -> do
liftIO . putStr . w $ []
keys <- liftIO getLine
runDrawIO cfg (GameState (Just keys)) (DrawT . f $ TimeStep)
通过添加IO
,我们可以定义在GameResourceT
中运行游戏。
runGameIO :: DrawT (GameT (GameResourceT IO)) a -> IO a
runGameIO = runGameResourceT . runDrawIO GameConfig (GameState Nothing)
最后,我们可以使用我们从一开始就想要的签名来编写runGame
。
runGame :: (forall m. MonadGame m => m a) -> IO a
runGame x = runGameIO x
此示例在5秒后请求最后一次输入的反转,并显示每帧都有可用数据的所有内容。
example :: MonadGame m => m ()
example = go []
where
go handles = do
handles <- dump handles
state <- getState
handles <- case keys state of
Nothing -> return handles
Just x -> do
handle <- requestResource ((threadDelay 5000000 >>) . return . reverse $ x)
return ((x,handle):handles)
draw
go handles
dump [] = return []
dump ((name, handle):xs) = do
resource <- handle
case resource of
Nothing -> liftM ((name,handle):) $ dump xs
Just contents -> do
output . GameOutput $ (name ++) . ("\n" ++) . (contents ++) . ("\n" ++)
dump xs
main = runGameIO example
答案 2 :(得分:2)
您可能想查找MVar
s:http://hackage.haskell.org/package/base-4.7.0.1/docs/Control-Concurrent-MVar.html。
tryReadMVar :: MVar a -> IO (Maybe a)
为您提供ResourceRequest
和
putMVar :: MVar a -> a -> IO ()
可用于在线程末尾按结果。像(忽略newtypes等):
requestResourceImpl :: IO a -> IO (IO (Maybe a))
requestResourceImpl a = do
mv <- newEmptyMVar
forkIO $ do
x <- a
putMVar mv x
return $ tryReadMVar mv
这不处理a
抛出异常等情况;如果a
确实会引发异常,那么结果ResourceRequest
将永远不会将资源报告为可用。
我强烈建议将GameMonad
设为抽象类型。您可以将其设为newtype
(如有必要,您可以添加deriving MonadReader
等)。然后你不导出它的构造函数;相反,定义像requestResource
这样的抽象操作并将其导出。