组成 monad 变压器堆栈

问题描述 投票:0回答:0

假设我有两个 Haskell 库,每个库都有一种用于计算的类型,

LibA
LibB
。它们都是堆叠在 IO monad 之上的 monad,使用
ReaderT
monad transformer 实现:

import Control.Monad.IO.Class (MonadIO)
import qualified Control.Monad.Reader as MR
import Control.Monad.Trans.Reader (runReaderT)

newtype LibA a = LibA (MR.ReaderT String IO a)
  deriving (Functor, Applicative, Monad, MonadIO)

newtype LibB a = LibB (MR.ReaderT String IO a)
  deriving (Functor, Applicative, Monad, MonadIO)

用这些库表达的程序用

runReaderT
执行:

runA :: String -> LibA a -> IO a
runA s (LibA x) =
  runReaderT x s

runB :: String -> LibB b -> IO b
runB s (LibB x) =
  runReaderT x s

现在假设我想编写一个使用这两个库的应用程序。

独立使用时,这些工作:

runA "conf" (return (2 :: Int)) :: IO Int
runB "conf" (return "foo") :: IO String

但是,在另一个中使用一个不会:

runA "conf" $ do
  -- Expected: LibA String, Actual: IO String
  runB "conf" (return "foo")

这是因为

runB
在 IO monad 中返回。

所以我可以用

LibA
将它提升到
liftIO

runA "conf" $ do
  liftIO (runB "conf" (return "foo"))

这是如何组合在一起的:

我有两个问题:(1)组合性,和(2)资源效率。

组合性

假设一个应用程序需要以交错的方式使用 monads

LibA
LibB
的计算。代码变得笨拙:

runA "conf" $ do
  liftIO $
    runB "conf" $ do
      liftIO $
        runA "conf" $ do
          liftIO $
            runB "conf" $ do
              return "foo"

资源效率

对于现实世界的库,

runA
runB
可能会在运行应用程序代码之前初始化资源。例如。建立然后释放与 Web 服务器、文件 IO 等的连接。深度嵌套的
runA
runB
调用将创建大量不必要的资源处理。

解决方案?

我所追求的是一种可组合的方式来交错

LibA
LibB
计算而无需嵌套
liftIO
调用,并且无需多次创建/释放 IO 资源。它们都直接堆叠在 IO monad 之上,所以这并没有超出想象的范围。我无法修改库实现。

例如像:

runAB :: (String, LibA a) -> (String, LibB b) -> IO (a, b)

这将为

LibA
LibB
动作创建一次相关的 IO 资源。

或者,如果无法实现资源效率,那么为了可读性,只需允许更直接地编写代码,就好像它们都堆叠在 IO monad 之上的一个 monad 堆栈中:

runA "conf" (runB "conf" (return "foo"))

有哪些选择?

haskell monads monad-transformers
© www.soinside.com 2019 - 2024. All rights reserved.