从 runDb 捕获异常

Posted

技术标签:

【中文标题】从 runDb 捕获异常【英文标题】:Catching an Exception from runDb 【发布时间】:2015-07-11 17:30:17 【问题描述】:

这是我之前帖子的后续。 MaybeT and Transactions in runDb

我认为这将是一件简单的事情,但我已经尝试了一天多,但仍然没有取得太大进展。所以想我会放弃并问!

我刚刚在我之前的代码中添加了一个try 函数(来自Control.Exception.Lifted),但我无法获取代码来进行类型检查。 catchhandle 等变体也有类似的问题。

eauth <- LiftIO (
           try( runDb $ do
                ma <- runMaybeT $ do
                   valid <- ... 
                case ma of
                  Just a -> return a
                  Nothing -> liftIO $ throwIO MyException
            )  :: IO (Either MyException Auth) 
          )
case eauth of
    Right auth -> return auth
    Left _     -> lift $ left err400  errBody = "Could not create user"

我的runDb 看起来像这样(我还尝试了一个删除liftIO 的变体):

runDb query = do
    pool <- asks getPool
    liftIO $ runSqlPool query pool

我收到此错误:

No instance for (Control.Monad.Reader.Class.MonadReader Config IO)
  arising from a use of ‘runDb’
In the expression: runDb
In the first argument of ‘try’, namely
  ‘(runDb
    $ do  ma <- runMaybeT ... 

我在仆人处理程序中运行,我的返回类型是AppM Auth

 type AppM = ReaderT Config (EitherT ServantErr IO)

我尝试了许多举重组合,但似乎没有帮助。我想我会借此机会从头开始弄清楚事情,我也碰壁了。如果有人可以建议您如何得出答案,那对我来说将是非常有启发性的。

这是我的思考过程:

    我看到runSqlConn :: MonadBaseControl IO m =&gt; SqlPersistT m a -&gt; Connection -&gt; m a 所以这似乎暗示它将在 IO monad 中,这意味着 try 应该可以工作 我想检查MonadBaseControl 的定义,它有class MonadBase b m =&gt; MonadBaseControl b m | m -&gt; b。在这一点上,我很困惑。这个函数依赖逻辑似乎是建议类型m 指示b 将是什么,但在前一个b 被指定为IO。 我检查了MonadBase,这也没有给我任何线索。 我检查了SqlPersistT,也没有任何线索。 我将问题简化为非常简单的问题,例如result &lt;- liftIO (try (evaluate (5 `div` 0)) :: IO (Either SomeException Int)),并且成功了。所以这个时候我就更加迷茫了。 runDb 不能在 IO 中工作,所以我的原始代码不应该同样工作吗?

我认为我可以通过回溯来解决这个问题,但我的 Haskell 知识水平似乎不足以解决问题的根源。欣赏人们是否可以提供逐步指导以得出正确的解决方案。

谢谢!

【问题讨论】:

LiftIO 是不是错字,应该是liftIOtry 也无法更改它正在运行的 monad(runDB 没有返回 IO,而您正试图让它返回 IO)。事实上,看起来try 可能对runDB 本身也有效。 您应该为runDb 提供类型签名。 @Guvante 的评论和我的回答都应该清楚地表明 runDB 不会返回 IO。由于您使用的异常只有一个值,因此 Maybe/MaybeT monad 对于您正在做的事情来说已经足够强大了。请参阅我答案的最后一部分。如果您仍然遇到问题,您应该在 runMaybeT 块中包含您省略的代码。 @Guvante,你说得对,LiftIO 是一个错字,你的观察也是正确的。由于我的类型签名,我最终陷入了兔子洞。 【参考方案1】:

try 的一般类型签名:

(MonadBaseControl IO m, Exception e) => m a -> m (Either e a)

try 的专用类型签名(如您的代码中所示):

IO Auth -> IO (Either MyException Auth)

因此,作为 try 参数的一元值具有以下类型:

IO Auth

上面列出的所有内容,您可能已经了解。如果我们查看您的 runDb 的类型签名,我们会得到:

runDb :: (MonadReader Config m, MonadIO m) => SqlPersistT m a -> m a

我不得不猜测,因为您没有提供类型签名,但可能就是这样。所以现在,问题应该清楚一点了。您正在尝试使用runDb 为应该在IO 中的东西创建一元值。但是IO 不能满足您需要的MonadReader Config 实例。

为了让错误更清楚,让我们让runDb 更加单态。你可以给它这个类型签名:

type AppM = ReaderT Config (EitherT ServantErr IO)
runDb :: SqlPersistT AppM a -> AppM a

现在如果你尝试编译你的代码,你会得到一个更好的错误。而不是告诉你

No instance for (Control.Monad.Reader.Class.MonadReader Config IO)

它会告诉你IO 不匹配AppM(尽管它可能会扩展类型同义词)。实际上,这意味着您无法神奇地从IO 中获取数据库连接的共享池。您需要到处传递它的ReaderT Config

我能想到的最简单的解决方法是在不需要的地方停止使用异常:

mauth <- runDb $ runMaybeT $ do
            ... -- Same stuff you were doing earlier 
case mauth of
    Just auth -> return auth
    Nothing   -> lift $ left err400  errBody = "Could not create user"

【讨论】:

感谢@Andrew 的详细回复。我也在 Reddit 上发布了这个,有人建议你和@Guvante 也提到过。简短的故事是我为try 提供了类型签名来抑制一条错误消息,但它会在以后导致更多问题。只要我将类型签名移至 case 表达式,只需删除 liftIO 并直接使用 try 即可。 reddit.com/r/haskell/comments/3d5kdm/… @Ecognium 确实有效,但正如我在上一个代码块中试图解释的那样,您完全没有必要使用 try。您正在将Maybe 转换为Exception,将Exception 捕获为Either,然后进行模式匹配以返回Exception。我的建议是只使用MaybeT 提前终止,然后对其结果进行模式匹配以将其变为异常。如果您将函数的全部内容放在某个地方(可能是另一个问题),那么我可以更好地向您展示我的意思。 谢谢。我抛出异常的原因是这是“回滚”事务的唯一方法。我之前只是使用 MaybeT,Nothing 值不会回滚,所以需要throwIO Exception。我的MaybeT 中有一堆插入语句,当然你不会从我的问题中知道。 作为替代方案,还有transactionUndo,它可以在不使用特殊异常处理行为的情况下回滚事务。以防万一您需要它。 哦,太好了。我根本不知道。我会尝试使用它而不是 throwIO 并且可能会使事情变得更清洁。

以上是关于从 runDb 捕获异常的主要内容,如果未能解决你的问题,请参考以下文章

如何从线程中捕获异常

从 Guzzle 中捕获异常

从线程捕获异常时退出程序

求人指点下如何捕获SQL连接异常

如何捕获从 iframe 内部抛出的异常?

如何从 wxPython 应用程序中捕获所有异常?