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

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 的一种方法是将我的ConsumerPrologueGLConsumerEpilogueGL都带入我的状态单子中,但这似乎是错误的,因为这些函数不需要(也不应该)与其交互;所有这一切只是为了减少代码噪音。

我能想到的另一个选择是简单地创建函数的提升版本并在ConsumerRenderStageGL'中调用它们- 这将减少代码噪音,但在执行/评估中是相同的。

第三个选项,即我的logGLErrors的工作原理,是我使用了一个类型类,该类型类为 IO 和我的状态 monad 定义了一个实例。

我期待阅读您的意见、建议和实践。

提前致谢!

Jon*_*rdy 5

有几种解决方案。一个常见的做法是做出基本操作,MonadIO m => m \xe2\x80\xa6而不是IO \xe2\x80\xa6

\n
consumePrologueGL :: (MonadIO m) => Prologue -> m ()\nconsumePrologueGL p = liftIO $ do\n  \xe2\x80\xa6\n
Run Code Online (Sandbox Code Playgroud)\n

然后你可以在StateT RenderStageContext IO ()不包装的情况下使用它们,因为MonadIO m => MonadIO (StateT s m),当然恒等函数在MonadIO IO哪里。liftIO

\n

您还可以StateT使用MonadStatefrom对该部分进行抽象mtl,因此,如果您在其上方/下方添加另一个变压器,则 \xe2\x80\x99t 不会遇到与提升 from/to 相同的问题StateT

\n
pushDebugGroupGL\n  :: (MonadIO m, MonadState RenderStageContext m)\n  => String -> m ()\n
Run Code Online (Sandbox Code Playgroud)\n

一般来说,具体的类型堆栈transformers可以了,它只是为了方便起见而帮助包装所有基本操作,以便所有的都lift在一个地方。

\n

mtl有助于lift完全消除代码中的噪音,并且在多态类型中工作m意味着您必须声明函数实际使用哪些效果,并且可以替换所有效果(除了MonadIO)的不同实现以进行测试。如果您的效果类型很少,那么使用 monad 转换器作为效果系统会很棒。如果你想要更细粒度或更灵活的东西,你\xe2\x80\x99将开始触及使人们转而寻求代数效应的痛点。

\n

它还值得评估您是否需要StateT超过IO. 通常,如果您\xe2\x80\x99位于 中IO,则您不需要\xe2\x80\x99t 提供的纯状态StateT,因此StateT MutableState IO您不妨使用ReaderT (IORef MutableState) IO

\n

它\xe2\x80\x99s 也可以使其(或其newtype包装器)成为 的实例,因此使用// 的代码MonadState MutableState甚至不需要\xe2\x80\x99t 进行更改:getputmodify

\n
{-# 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\n
Run Code Online (Sandbox Code Playgroud)\n

ReaderT&的这种组合IO是一种非常常见的设计模式。

\n