【问题标题】:Inserting ErrorT at the base of transformer stack在变压器堆栈的底部插入 ErrorT
【发布时间】:2013-12-22 04:16:59
【问题描述】:

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 应该是一个无限循环。

【问题讨论】:

  • 外部真的应该是无限循环吗?
  • @GaneshSittampalam 为什么不呢? Haskell 很懒,所以 runReaderT 会产生一个有用的 IO 动作
  • IO 操作可能很有用,因为它在每次迭代或类似情况下都会与用户交互 - 只是我从问题中认为外部应该类似于内部但删除了 ErrorT 字符串。

标签: haskell error-handling monads monad-transformers


【解决方案1】:

请注意,正如 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 -&gt; IO a 类型的函数——即处理任何类型的值,而不仅仅是()。这是因为依赖于 monad 堆栈的其余部分可能意味着当您到达 ErrorT 时的实际返回类型与您开始时的返回类型不同。

在失败的情况下,我们没有a 类型的值,因此一种选择是失败。

在您的原始代码中,您还可以在 outer 中无限循环,这不会。

【讨论】:

  • 谢谢。我想我应该更清楚地说明我希望outer 是一个无限循环。我想我最好的选择是使用类似catchAndIgnoreException $ (hoist runErrorTWithPrint) inner
  • 感觉很丑,但我想它会起作用。我看到的其他选项是:(a)将“失败错误”更改为“返回未定义”并希望堆栈中没有任何内容实际检查返回值。 (b) 尝试自己编写类似 MFunctor/hoist 的东西,但类型更受限制,并且使用更小的单子集,因此 runErrorTWithPrint 不需要是多态的。不过,我怀疑 StateT 会击败这两种选择。
  • 我同意,这太丑了。但由于我的大学学期结束后这个项目就会死掉,它会做的。我刚刚检查过返回 undefined 不起作用。
【解决方案2】:

这里的正确答案是“不要那样做”。

这里的问题是您选择了分层。如果您将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 的路径,但请仔细考虑您是否真的需要在您描述的情况下变形!​​p>

【讨论】:

  • 谢谢。不幸的是,正如我刚刚在问题中所写,我在一些堆栈中使用StateT,并且宁愿在它下方使用ErrorT,而不是在顶部使用。
  • 那么您可能会遇到涉及hoist 的解决方案。
猜你喜欢
  • 2015-01-03
  • 2018-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-27
  • 2013-12-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多