问题描述
在此之前,我会说我是一个新手 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
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
虽然大多数人并不生活在 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)
我的问题是:有没有办法在 consumeRenderStageGL' 中隐藏 liftIO,更重要的是,这是一个好主意还是一个坏主意?强>
我能想到的隐藏/摆脱liftIO的一种方法是将我的consumePrologueGL和consumeEpilogueGL都带入我的状态monad中,但这似乎是错误的这些函数不需要(也不应该)与之交互;所有这一切只是为了减少代码噪音。
我能想到的另一个选择是简单地创建函数的提升版本并在 consumeRenderStageGL' 中调用它们 - 这将减少代码噪音,但在执行/评估中是相同的。 >
第三个选项,即我的 logGLErrors 的工作方式,是我使用了一个类型类,该类为 IO 和我的状态 monad 定义了一个实例。
我期待阅读您的意见、建议和做法。
提前致谢!
解决方法
有几个解决方案。一个常见的做法是让您的基本操作 MonadIO m => m …
而不是 IO …
:
consumePrologueGL :: (MonadIO m) => Prologue -> m ()
consumePrologueGL p = liftIO $ do
…
然后您可以在 StateT RenderStageContext IO ()
中使用它们而无需换行,因为 MonadIO m => MonadIO (StateT s m)
,当然还有 MonadIO IO
其中 liftIO
是身份函数。
您还可以使用 StateT
中的 MonadState
对 mtl
部分进行抽象,因此如果您在其上方/下方添加另一个转换器,则不会遇到从/to StateT
。
pushDebugGroupGL
:: (MonadIO m,MonadState RenderStageContext m)
=> String -> m ()
通常,transformers
类型的具体堆栈很好,它只是为了方便包装您的所有基本操作,以便所有 lift
都在一个地方.
mtl
有助于从您的代码中完全消除 lift
噪音,并且在多态类型 m
中工作意味着您必须声明函数实际使用的效果,并且可以替换不同的实现用于测试的所有效果(MonadIO
除外)。如果您的效果类型很少,那么使用 monad 转换器作为这样的效果系统会很棒;如果您想要更细粒度或更灵活的东西,您将开始解决让人们达到代数效应的痛点。
还需要评估您是否需要 StateT
而不是 IO
。通常,如果您在 IO
中,您不需要 StateT
提供的纯状态,因此您不妨使用 StateT MutableState IO
代替 ReaderT (IORef MutableState) IO
。
也可以将它(或它的 newtype
包装器)设为 MonadState MutableState
的实例,因此您的代码使用 get
/put
/{{1} } 甚至不需要改变:
modify
{-# Language GeneralizedNewtypeDeriving #-}
import Data.Coerce (coerce)
newtype MutT s m a = MutT
{ getMutT :: ReaderT (IORef s) m a }
deriving
( Alternative,Applicative,Functor,Monad,MonadIO,MonadTrans
)
evalMutT :: MutT s m a -> IORef s -> m a
evalMutT = coerce
instance (MonadIO m) => MonadState s (MutT s m) where
state f = MutT $ do
r <- ask
liftIO $ do
-- NB: possibly lazier than you want.
(a,s) <- f <$> readIORef r
a <$ writeIORef r s
和 ReaderT
的这种组合是一种非常常见的设计模式。