如何将IO操作的结果注入非IO monadic计算中

时间:2014-12-10 00:14:33

标签: haskell monads monad-transformers

我有一点建筑问题,我想知道是否有一个可以帮助我的常见模式或抽象。我是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是否有类似这种编程模式的抽象或习语?是我想要的不可能吗?

3 个答案:

答案 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不包含MonadIOrunGame的用户通常就无法使用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中的IOMVar来处理资源。我们创建一个变换器,所以我们有一个类型来附加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这样的抽象操作并将其导出。