在t IO a
monad中运行类型为t (ErrorT String IO) a
的代码的最佳方式是什么?考虑以下代码:
module Sample where
import System.IO
import Control.Monad.Reader
import Control.Monad.Error
type Env = String
inner :: ReaderT Env (ErrorT String IO) ()
inner = do
s <- ask
fail s
outer :: ReaderT Env IO ()
outer = do
env <- ask
res <- lift $ runErrorT $ runReaderT inner env
case res of
Left err -> liftIO $ hPutStrLn stderr err
Right _ -> return ()
outer
这是可行的,但我一直在寻找一种更优雅的方式,在堆栈的底部插入ErrorT。尤其是我在项目中使用了几个不同的monad转换器堆栈,并且为每个堆栈编写上面的内容非常乏味。
我在找这样的东西:
outer :: ReaderT Env IO ()
outer = do
res <- (hoist runErrorT) inner
...
但由于类型不匹配,我无法使用hoist
。
编辑:
我在一些堆栈中使用StateT
,这就是尝试将ErrorT
放在底部而不是顶部的原因。
outer
被认为是一个无限循环。
请注意,正如Edward所说,将ErrorT
放在堆栈的顶部而不是底部通常会简单得多。
这可能会改变堆栈的语义,至少对于比ReaderT
更复杂的转换器来说是这样——例如,如果堆栈中有StateT
,那么在ErrorT
位于底部的情况下,当出现错误时,对状态的更改将被回滚,而在ErrorT
位于顶部的情况下,当发生错误时,将保留对状态的修改。
如果你真的在底部需要它,那么像这样的东西就会通过类型检查器:
import Control.Monad.Error
import Control.Monad.Morph
import System.IO
toOuter :: MFunctor t => t (ErrorT String IO) a -> t IO a
toOuter = hoist runErrorTWithPrint
runErrorTWithPrint :: ErrorT String IO a -> IO a
runErrorTWithPrint m = do
res <- runErrorT m
case res of
Left err -> do
hPutStrLn stderr err
fail err
Right v -> return v
请注意,当内部计算失败时,它会调用fail
,而上面的代码并不是这样做的。
主要原因是要使用hoist
,我们需要提供forall a . ErrorT String IO a -> IO a
类型的函数,即处理任何类型的值,而不仅仅是()
。这是因为依赖于monad堆栈的其余部分可能意味着当您到达ErrorT
时,实际的返回类型与您开始使用的返回类型不同。
在失败的情况下,我们没有类型为a
的值,因此一个选项是失败。
在您的原始代码中,您还可以在outer
中无限循环,但这并不能做到
这里的正确答案是"不要那样做"。
这里的问题是你在挑选层次。如果您将Error
移到外部,则在这种情况下,它将对fail
表现正常。一般来说,将变压器堆栈视为某种量子波形,直到最后一分钟才应该崩溃。
inner :: MonadReader Env m => m ()
inner = do
s <- ask
fail s
outer :: (MonadReader Env m, MonadIO m) => m ()
outer = do
res <- runErrorT inner
case res of
Left err -> liftIO $ hPutStrLn stderr err
Right _ -> return ()
outer
注意每件事都变得简单多了。无吊装,无明显吊装,nada。inner
在另一个monad中运行,我们已经扩展了当前的monad,不管它是什么,外部有ErrorT。
通过不显式地选择堆栈,可以最大限度地增加可以使用代码的情况数量。
如果你绝对必须这样做,那么就按照Ganesh的路线去做,但要认真思考你是否真的需要在你描述的情况下变形!