Alt*_*r93 4 haskell monad-transformers
我会在序言中说,我是一名 Haskell 程序员新手(多年来偶尔对其进行修改),但在 OOO 和命令式编程方面,我已经有好几年的时间了。我目前正在学习如何使用 monad 并通过使用 monad 转换器将它们组合起来(假设我已经找到了正确的术语)。
虽然我能够将事物组装/链接在一起,但我发现很难对什么是最好的方式和风格以及如何最好地组装/编写这些交互建立直觉。
具体来说,我很想知道使用 lift/liftIO 以及两者之间的任何风味的最佳实践(或至少是你的实践)是什么,以及是否有方法(和好处)隐藏它们,因为我发现它们相当“吵闹” 。
下面是一个示例片段,我将其放在一起来说明我的意思:
consumeRenderStageGL' :: RenderStage -> StateT RenderStageContext IO ()
consumeRenderStageGL' r = do
pushDebugGroupGL (name r)
liftIO $ consumePrologueGL ( prologue r )
liftIO $ consumeEpilogueGL ( epilogue r )
consumeStreamGL ( stream r )
liftIO $ popDebugGroupGL
Run Code Online (Sandbox Code Playgroud)
它调用的一些函数利用了状态 monad :
pushDebugGroupGL :: String -> StateT RenderStageContext IO ()
pushDebugGroupGL tag = do
currentDebugMessageID <- gets debugMessageID
liftIO $ GL.pushDebugGroup GL.DebugSourceApplication (GL.DebugMessageID currentDebugMessageID) tag
modify (\fc -> fc { debugMessageID = (currentDebugMessageID + 1) })
consumeStreamGL :: Stream -> StateT RenderStageContext IO ()
consumeStreamGL s = do
mapM_ consumeTokenGL s
logGLErrors
Run Code Online (Sandbox Code Playgroud)
虽然大多数都没有,只是生活在 IO 中(这意味着它们必须被解除):
consumePrologueGL :: Prologue -> IO ()
consumePrologueGL p = do
colourClearFlag <- setupAndReturnClearFlag GL.ColorBuffer ( clearColour p ) (\(Colour4 r g b a) -> GL.clearColor $= (GL.Color4 r g b a))
depthClearFlag <- setupAndReturnClearFlag GL.DepthBuffer ( clearDepth p ) (\d -> GL.clearDepthf $= d)
stencilClearFlag <- setupAndReturnClearFlag GL.StencilBuffer ( clearStencil p ) (\s -> GL.clearStencil $= fromIntegral s)
GL.clear $ catMaybes [colourClearFlag, depthClearFlag, stencilClearFlag]
logGLErrors
where
setupAndReturnClearFlag flag mValue function = case mValue of
Nothing -> return Nothing
Just value -> (function value) >> return (Just flag)
Run Code Online (Sandbox Code Playgroud)
我的问题是:有没有什么方法可以隐藏ConsumerRenderStageGL中的liftIO,更重要的是,这是一个好主意还是坏主意?
我可以想到隐藏/摆脱 liftIO 的一种方法是将我的ConsumerPrologueGL和ConsumerEpilogueGL都带入我的状态单子中,但这似乎是错误的,因为这些函数不需要(也不应该)与其交互;所有这一切只是为了减少代码噪音。
我能想到的另一个选择是简单地创建函数的提升版本并在ConsumerRenderStageGL'中调用它们- 这将减少代码噪音,但在执行/评估中是相同的。
第三个选项,即我的logGLErrors的工作原理,是我使用了一个类型类,该类型类为 IO 和我的状态 monad 定义了一个实例。
我期待阅读您的意见、建议和实践。
提前致谢!
有几种解决方案。一个常见的做法是做出基本操作,MonadIO m => m \xe2\x80\xa6而不是IO \xe2\x80\xa6:
consumePrologueGL :: (MonadIO m) => Prologue -> m ()\nconsumePrologueGL p = liftIO $ do\n \xe2\x80\xa6\nRun Code Online (Sandbox Code Playgroud)\n然后你可以在StateT RenderStageContext IO ()不包装的情况下使用它们,因为MonadIO m => MonadIO (StateT s m),当然恒等函数在MonadIO IO哪里。liftIO
您还可以StateT使用MonadStatefrom对该部分进行抽象mtl,因此,如果您在其上方/下方添加另一个变压器,则 \xe2\x80\x99t 不会遇到与提升 from/to 相同的问题StateT。
pushDebugGroupGL\n :: (MonadIO m, MonadState RenderStageContext m)\n => String -> m ()\nRun Code Online (Sandbox Code Playgroud)\n一般来说,具体的类型堆栈transformers就可以了,它只是为了方便起见而帮助包装所有基本操作,以便所有的都lift在一个地方。
mtl有助于lift完全消除代码中的噪音,并且在多态类型中工作m意味着您必须声明函数实际使用哪些效果,并且可以替换所有效果(除了MonadIO)的不同实现以进行测试。如果您的效果类型很少,那么使用 monad 转换器作为效果系统会很棒。如果你想要更细粒度或更灵活的东西,你\xe2\x80\x99将开始触及使人们转而寻求代数效应的痛点。
它还值得评估您是否需要StateT超过IO. 通常,如果您\xe2\x80\x99位于 中IO,则您不需要\xe2\x80\x99t 提供的纯状态StateT,因此StateT MutableState IO您不妨使用ReaderT (IORef MutableState) IO。
它\xe2\x80\x99s 也可以使其(或其newtype包装器)成为 的实例,因此使用// 的代码MonadState MutableState甚至不需要\xe2\x80\x99t 进行更改:getputmodify
{-# Language GeneralizedNewtypeDeriving #-}\n\nimport Data.Coerce (coerce)\n\nnewtype MutT s m a = MutT\n { getMutT :: ReaderT (IORef s) m a }\n deriving\n ( Alternative\n , Applicative\n , Functor\n , Monad\n , MonadIO\n , MonadTrans\n )\n\nevalMutT :: MutT s m a -> IORef s -> m a\nevalMutT = coerce\n\ninstance (MonadIO m) => MonadState s (MutT s m) where\n state f = MutT $ do\n r <- ask\n liftIO $ do\n -- NB: possibly lazier than you want.\n (a, s) <- f <$> readIORef r\n a <$ writeIORef r s\nRun Code Online (Sandbox Code Playgroud)\nReaderT&的这种组合IO是一种非常常见的设计模式。