簡體   English   中英

monad 轉換器的最佳實踐:隱藏或不隱藏“liftIO”

[英]Best practices with monad transformers : to hide or not to hide '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

它調用的一些函數使用了 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

雖然大多數人沒有,只是住在 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)

我的問題是:有什么方法可以在consumerRenderStageGL'中隱藏liftIO ,更重要的是,這是一個好主意還是壞主意?

我可以想到隱藏/擺脫liftIO的一種方法是將我的consumePrologueGLconsumeEpilogueGL都帶入我的state monad,但這似乎是錯誤的,因為那些function不需要(也不應該)與它交互; 所有這些只是為了減少代碼噪音。

我能想到的另一個選擇是簡單地創建函數的提升版本並在consumeRenderStageGL'中調用它們——這將減少代碼噪音,但在執行/評估中是相同的。

第三個選項,這就是我的logGLErrors的工作方式,我使用了 class 類型,該類型具有為 IO 和我的 state monads 定義的實例。

我期待閱讀您的意見、建議和做法。

提前致謝!

有幾個解決方案。 一個常見的做法是讓你的基本動作MonadIO m => m …而不是IO …

consumePrologueGL :: (MonadIO m) => Prologue -> m ()
consumePrologueGL p = liftIO $ do
  …

然后,您可以在StateT RenderStageContext IO ()中使用它們而無需包裝,因為MonadIO m => MonadIO (StateT sm) ,當然還有MonadIO IO其中liftIO是身份 ZC1C4F145264E179A8。

您還可以使用mtl中的MonadStateStateT部分進行抽象,因此如果您在其上方/下方添加另一個轉換器,您將不會遇到從/到StateT的同樣問題。

pushDebugGroupGL
  :: (MonadIO m, MonadState RenderStageContext m)
  => String -> m ()

一般來說,一堆具體的transformers類型就可以了,它只是為了方便將所有基本操作包裝起來,這樣所有的lift都在一個地方。

mtl有助於從您的代碼中完全消除lift噪聲,並且在多態類型m中工作意味着您必須聲明 function 實際使用的效果,並且可以替換所有效果的不同實現(除了MonadIO )進行測試。 如果您的效果類型很少,那么使用 monad 轉換器作為這樣的效果系統非常棒; 如果您想要更細粒度或更靈活的東西,您將開始觸及使人們達到代數效應的痛點。

還值得評估您是否需要StateT而不是IO Typically if you're in IO , you don't need the pure state offered by StateT , so instead of StateT MutableState IO you might as well use ReaderT (IORef MutableState) IO .

也可以使它(或它的新類型包裝器)成為newtype MonadState MutableState的實例,因此您使用get / put / 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

ReaderTIO的這種組合是一種非常常見的設計模式。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM