使用带外数据编写monad(也就是说,并行编写monad)

时间:2012-02-10 05:54:51

标签: haskell monads

我正在编写一个名为GL的包装OpenGL的monad,我希望能够查询计算以获得它可能需要的每个纹理的列表。

这是一个解决的问题吗?我在为GL编写Monad实例时遇到了很多麻烦。

这是我到目前为止所尝试的:

-- GL should be able to be inspected for its HashSet without running the computation.
newtype GL a = GL (S.HashSet String) (IO a)

instance Monad (GL a) where
    return = GL S.empty . return -- Calls IO.return
    (>>=) (GL textures action) f = -- What goes here?
但是我在吠叫错了树吗?它并不真正作为monad工作,因为我必须在运行它之前查询它。我应该用什么呢?我真的喜欢使用do-notation。

我认为这可以分解为:如何并行组合两个monad,然后独立运行它们?

2 个答案:

答案 0 :(得分:7)

你的GL类型的问题在于"计算的结果" a依赖于IO操作,因此您无法实现monad实例,您可以在其中计算最终纹理HashSet而无需运行IO操作。

正确的解决方案取决于您如何使用GL monad的详细信息,但假设您可以决定使用哪些纹理而不运行IO-actions,那么您可以使用类似这样的类型

type GL a = WriterT (Set String) (Writer (IO ())) a

即。你使用两个嵌套的编写器monad,一个用于纹理,一个用于累积IO动作。生成的monad堆栈分两个阶段运行,您可以在不执行IO操作的情况下获取最终纹理集。

不幸的是,Writer仅适用于幺半群,因此我们需要首先为Monoid定义IO ()个实例。

{-# LANGUAGE FlexibleInstances #-}

import Data.Monoid

instance Monoid (IO ()) where
    mempty = return ()
    mappend = (>>)

现在,您可以编写一个注册新纹理的函数,如下所示:

addTexture :: String -> GL ()
addTexture = tell . S.singleton

另一个缓存IO动作以便稍后执行的函数

addIO :: IO () -> GL ()
addIO = lift . tell

这是用于运行GL monad的实用程序功能

runGL :: GL a -> (a, Set String, IO ())
runGL gl = let iow = runWriterT gl
               ((a, textures), io) = runWriter iow
            in (a, textures, io)

这会返回一个包含三个元素的元组:计算的结果值,累积纹理集和累积的io动作。请注意,此时,元组中的IO ()值仅描述了操作,并且尚未执行任何操作(例如绘图操作)。

我不确定这是否涵盖了您的用例,但希望它能为您提供有关如何构建合适的monad堆栈的一些想法。如果您需要更多帮助,请提供一些有关如何实际使用GL monad的示例。

这是我测试的完整代码。请注意,我使用了Set类型而不是HashSet,因为根据hashmap库的文档,不推荐使用HashSet名称。

{-# LANGUAGE FlexibleInstances #-}

import Control.Monad.Writer
import Data.Monoid
import Data.HashSet (Set)
import qualified Data.HashSet as S

instance Monoid (IO ()) where
    mempty = return ()
    mappend = (>>)

type GL a = WriterT (Set String) (Writer (IO ())) a

addTexture :: String -> GL ()
addTexture = tell . S.singleton

addIO :: IO () -> GL ()
addIO = lift . tell

runGL :: GL a -> (a, Set String, IO ())
runGL gl = let iow = runWriterT gl
               ((a, textures), io) = runWriter iow
            in (a, textures, io)

编辑:如果您按照dave4420的建议将IO效果包装成新类型,也可以避免语言扩展。

import Control.Monad.Writer
import Data.Monoid
import Data.HashSet (Set)
import qualified Data.HashSet as S

newtype WrapIO = WrapIO { unwrapIO :: IO () }

instance Monoid WrapIO where
    mempty = WrapIO $ return ()
    WrapIO a `mappend` WrapIO b = WrapIO $ a >> b

type GL a = WriterT (Set String) (Writer WrapIO) a

addTexture :: String -> GL ()
addTexture = tell . S.singleton

addIO :: IO () -> GL ()
addIO = lift . tell . WrapIO

runGL :: GL a -> (a, Set String, IO ())
runGL gl = let iow = runWriterT gl
               ((a, textures), WrapIO io) = runWriter iow
            in (a, textures, io)

答案 1 :(得分:3)

如果你需要在没有实际运行它们的情况下推断出有关你的计算的东西,那么应用仿函数往往比monad更好,因为它们的效果具有静态结构。

这是因为使用applicative functor,你的排序操作方法仅限于(<*>) :: f (a -> b) -> f a -> f b,所以第一个参数中的函数不能改变会发生什么副作用,不像(=<<) :: (a -> m b) -> m a -> m b,其中函数参数是可以自由选择任何副作用,所以为了提取有关这些副作用的信息,你必须评估函数,这反过来需要前一个动作的结果等等,直到你几乎被迫运行整个的事情。

快速的应用实现看起来像这样:

data GL a = GL (S.HashSet String) (IO a)

instance Functor GL where
  fmap f (GL s x) = GL s (fmap f x)

instance Applicative GL where
  pure x = GL S.empty (pure x)
  (GL t0 f) <*> (GL t1 x) = GL (t0 `S.union` t1) (f <*> x)

当然,避免monad意味着你失去了一堆控制结构,因此你必须在你的仿函数中提供替换原语,例如你想允许条件,并确保你结合来自不同的信息分支正确。

whenGL :: GL Bool -> GL () -> GL ()
whenGL (GL t0 cond) (GL t1 body) = GL (t0 `S.union` t1) (cond >>= \b -> if b then body else return ())

总而言之,我认为应该可以使用applicative来做你想做的事情,但编程可能有点麻烦。特别是因为你在Control.Monad中失去了诸如do-notation和各种控制结构之类的东西。