monad 转换器的最佳实践:隐藏或不隐藏 'liftIO'

问题描述

在此之前,我会说我是一个新手 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

调用的一些函数使用了状态 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

虽然大多数人并不生活在 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的一种方法是将我的consumePrologueGLconsumeEpilogueGL都带入我的状态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 中的 MonadStatemtl 部分进行抽象,因此如果您在其上方/下方添加另一个转换器,则不会遇到从/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 的这种组合是一种非常常见的设计模式。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...