假设我有两个 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"))
有哪些选择?