I will preface this by saying that I am a novice Haskell programmer (tinkered with it sporadically over the years) but I have a fair few years on the counter when it comes to OOO and imperative programming. I am currently learning how to use monads & combining them via the use of monad transformers (assuming I've got the correct term).
While I am able to assemble/chain things together, I am finding it hard to build an intuition for what the best way and style is & how best to assemble/write those interactions.
Specifically, I am curious to know what the best practice (or at least your practice) is for using lift/liftIO and any flavour in between & if there is way (and benefit) to hide them as I am finding them rather 'noisy'.
Below is an example snippet which I have put together to illustrate what I mean :
consumeRenderStageGL' :: RenderStage -> StateT RenderStageContext IO ()
consumeRenderStageGL' r = do
pushDebugGroupGL (name r)
liftIO $ consumePrologueGL ( prologue r )
liftIO $ consumeEpilogueGL ( epilogue r )
consumeStreamGL ( stream r )
liftIO $ popDebugGroupGL
Some of the functions it calls make use of the state 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
While most don't and just live in IO (meaning they have to be lifted):
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)
My question is : is there any way to hide the liftIO in consumeRenderStageGL' and more importantly would this be a good or a bad idea?
One way I can think of hiding/getting rid of the liftIO is to bring both my consumePrologueGL & consumeEpilogueGL into my state monad but this seems wrong in the sense that those function do not need (and should not) interact with it ; all this just to reduce code noise.
The other option I can think of is to simply create the lifted version of the functions and call them in consumeRenderStageGL' - this would reduce the code noise but be identical in execution/evaluation.
The third option, which is how my logGLErrors works is that I have used a type class which has an instance defined for both IO & my state monads.
I look forward to reading what your opinions, advices and practices are.
Thanks in advance!