Haskell monad Transformer的最佳实践:隐藏或不隐藏';liftIO&x27;

Haskell monad Transformer的最佳实践:隐藏或不隐藏';liftIO&x27;,haskell,monad-transformers,Haskell,Monad Transformers,首先,我要说,我是一个Haskell程序员新手(多年来偶尔对其进行修补),但在OOO和命令式编程方面,我有相当几年的时间。我目前正在学习如何使用monad&通过使用monad变压器将它们结合起来(假设我有正确的术语) 虽然我能够将事物组合/链接在一起,但我发现很难建立一种直觉,知道什么是最好的方式和风格&如何最好地组合/编写这些交互 具体地说,我很想知道使用lift/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
pushDebugGroupGL::String->StateT RenderStageContext IO()
pushDebugGroupGL标记=do
currentDebugMessageID fc{debugMessageID=(currentDebugMessageID+1)})
ConsumerStreamGL::Stream->StateT RenderStageContext IO()
s=do
mapM_uu消费令牌
日志错误
而大多数人不这样做,只是生活在IO中(这意味着他们必须被提升):

consumerPrologueGL::Prologue->IO()
consumerprologuegl p=do
ColorClearFlag GL.clearColor$=(GL.Color4 r g b a))
深度ClearFlag GL.clearDepthf$=d)
stencilClearFlag GL.ClearTencil$=Froms)
GL.clear$catMaybes[彩色ClearFlag、深度ClearFlag、模具ClearFlag]
日志错误
哪里
setupAndReturnClearFlag标志mValue函数=的案例mValue
无->不返回任何内容
Just value->(函数值)>>返回(Just标志)
我的问题是:有没有办法将liftIO隐藏在consumerrenderstagegl'中,更重要的是,这是个好主意还是个坏主意?

我可以考虑隐藏/摆脱liftIO的一种方法是将我的consumePrologueGL&consumeEpilogueGL带到我的状态monad中,但这似乎是错误的,因为这些函数不需要(也不应该)与之交互;所有这些都只是为了减少代码噪音

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

第三个选项是myLoggleErrors的工作方式,我使用了一个类型类,该类为IO和my state Monad定义了一个实例

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


提前谢谢

有几种解决方案。一个常见的方法是使您的基本操作
MonadIO m=>m…
而不是
IO…

consumePrologueGL :: (MonadIO m) => Prologue -> m ()
consumePrologueGL p = liftIO $ do
  …
然后,由于
MonadIO m=>MonadIO(StateT s m)
,您可以在
StateT renderstageconext IO()
中使用它们,而无需包装,当然
MonadIO IO IO
中的
liftIO
是标识函数

您还可以使用
mtl
中的
MonadState
StateT
部分进行抽象,因此如果在其上方/下方添加另一个转换器,则不会出现从
StateT提升到
StateT
的问题

pushDebugGroupGL
  :: (MonadIO m, MonadState RenderStageContext m)
  => String -> m ()
一般来说,一堆具体的
变形金刚
类型就可以了,为了方便起见,它只是有助于包装所有的基本操作,使所有的
提升
都在一个地方

mtl
有助于从代码中完全消除
lift
噪音,在多态类型
m
中工作意味着您必须声明函数实际使用的效果,并且可以替换所有效果的不同实现(除了
MonadIO
)进行测试。使用单子变压器作为一个效果系统,如这是伟大的,如果你有几个类型的效果;如果您想要更细粒度或更灵活的东西,那么您将开始遇到让人们获得代数效果的痛点

还值得评估您是否需要
StateT
而不是
IO
。通常,如果您处于
IO
,则不需要
StateT
提供的纯状态,因此与其使用
StateT-MutableState IO
,不如使用
ReaderT(IORef-MutableState)IO

还可以将其(或其
newtype
包装器)作为
MonadState可变状态的实例,这样您使用
get
/
put
/
修改的代码甚至不需要更改:

{-# 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
{-#语言泛化newtypederiving}
导入数据。强制(强制)
新型MutT s m a=MutT
{getMutT::ReaderT(iorefs)ma}
衍生
(备选方案
,实用的
,函子
,单子
,MonadIO
,MonadTrans
)
评估工具::MutT s m a->IORef s->m a
evalMutT=强制
实例(MonadIO m)=>MonadState s(MutT s m),其中
州f=MutT$do

r最明显的方法是将
liftIO
下推到
consumerprologuegl
,使其
consumerprologuegl::MonadIO m=>Prologue->m()
有趣,我没有考虑到,如果直接在IO monad中使用liftIO(从评估的意义上讲)会是“免费的”吗?正确。我确实读过一些关于MonadIO与IO“团队”的帖子,尽管我无法确定在MonadIO中编写每一个IO函数是否被认为是过激/过度工程(就像试图在C++中编写模板类,当它只在非常特定的范围内使用时)注意,如果你在代码> IO < /CUD>中做了一部分
{-# 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