异常和monad变换器

时间:2014-09-09 20:16:00

标签: haskell monad-transformers

我正在使用EitherT monad变压器。将它与IO monad相结合,恐怕我会得到一个例外而且它不会被捕获。

事实上,例外只是通过:

import Control.Monad.Trans
import Control.Error
import System.Directory

main = runEitherT testEx >>= print

testEx :: EitherT String IO ()
testEx = lift $ removeFile "non existing filename"

EitherT完全适合该法案,以便向来电者传达错误。所以我想使用它,而不是抛出异常......

我从Control.Exception:

查看了try
try :: Exception e => IO a -> IO (Either e a) 

它看起来正是我想要的,它将适合我的EitherT IO堆栈......(可能添加了hoistEither并且可能fmapL并且它开始看起来很冗长)但是一个天真的lift $ try没有进行类型检查。

我确信这个问题已经解决了好几千次,但我找不到任何描述这个问题的好链接。这应该如何解决?

编辑通过“这应该如何解决”,我对惯用解决方案感兴趣,在haskell处理这个问题的标准方法是什么。从目前为止的答案来看,似乎惯用的方法是抛出异常并将其处理得更高。看起来有点反直觉,有两个控制和返回路径流,但它显然是它的意图。

3 个答案:

答案 0 :(得分:16)

我实际上认为EitherT 在这里做正确的事情。你想说的是" IO用于副作用,而EitherT用于例外。"但事实并非如此:IO 总是有可能导致异常,因此您所做的只是为您的API添加错误的安全感,并引入两种方式可以抛出异常而不是一种异常。此外,您不必使用SomeException所偏好的结构良好的IO,而是缩减为String,这会丢弃信息。

无论如何,如果你确信这是你想做的事情,那就不难了。它看起来像:

eres <- liftIO $ try x
case eres of
    Left e -> throwError $ show (e :: SomeException)
    Right x -> return x

但请注意,这也会吞噬异步异常,这通常是您想要做的事情。我认为更好的方法是enclosed-exceptions

答案 1 :(得分:5)

您不希望lift try计算,然后您会得到Exception e => EitherT a IO (Either e ())

testEx :: (Exception e, MonadTrans m) => m IO (Either e ())
testEx = lift . try $ fails

您不希望结果中出现错误,您希望将错误集成到EitherT中。您希望将try某些帖子与EitherT

进行整合
testEx :: (Exception e) => EitherT e IO ()
testEx = EitherT . try $ fails

我们一般会这样做,然后只获得你想要的信息。

将尝试与EitherT

集成

您可以提取将tryEitherT

进行整合的想法
tryIO :: (Exception e) => IO a -> EitherT e IO a
tryIO = EitherT . try

或者,对于任何潜在的MonadIO

tryIO :: (Exception e, MonadIO m) => IO a -> EitherT e m a
tryIO = EitherT . liftIO . try

tryIO与来自Control.Error的名称冲突。我无法为此提出其他名称。)

然后你可以说你愿意抓住任何例外。 SomeException将捕获所有异常。如果您只对特定例外感兴趣,请使用其他类型。有关详细信息,请参阅Control.Exception。如果你不确定你想要捕获什么,你可能只想抓住IOException;这是来自tryIO的{​​{1}};见最后一节。

Control.Error

您只想保留异常

的错误消息
anyException :: EitherT SomeException m a -> EitherT SomeException m a
anyException = id

然后你可以写

message :: (Show e, Functor m) => EitherT e m a -> EitherT String m a
message = bimapEitherT show id

将try与MonadError集成

您可以使用testEx :: EitherT String IO () testEx = message . anyException . tryIO $ fails tryMonadError内容与任何MonadError集成,以便穿透变换器堆栈。

MonadIO

您可以根据上一节中的import Control.Monad.Except tryIO :: (MonadError e m, MonadIO m, Exception e) => IO a -> m a tryIO = (>>= either throwError return) . liftIO . try testEx以及tryIO撰写anyException

message
来自Control.Error 的

tryIO

来自Control.Error的testEx :: EitherT String IO () testEx = message . anyException . tryIO $ fails 本质上是我们的第一个tryIO,除了它只捕获tryIO而不是任何异常。它实际上被定义为

IOException

我们可以将其与tryIO :: (MonadIO m) => IO a -> EitherT IOException m a tryIO = EitherT . liftIO . try 一起用于将message写为

testEx

答案 2 :(得分:1)

这是另一种简单的方法:让我们定义一个自定义monad变换器,就像定义了EitherT一样:

{-# LANGUAGE FlexibleInstances, FunctionalDependencies #-}
import Control.Arrow (left)
import Control.Exception
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Error
import Control.Monad.IO.Class

newtype ErrT a m b = ErrT { runErrT :: m (Either a b) }

instance (Monad m) => Monad (ErrT a m) where
    -- ...

instance (Monad m) => MonadError a (ErrT a m) where
    -- ...

instance MonadTrans (ErrT a) where
    lift = ErrT . liftM Right

以及相应的ApplicativeMonadMonadError个实例。

现在让我们添加一种方法,将IOError转换为我们的错误类型。我们可以为此设置一个类型类,以便我们可以自由使用变换器。

class FromIOError e where
    fromIOException :: IOError -> e

最后,我们将以MonadIO始终捕获liftIO并将其转换为左侧部分的纯数据类型的方式实施IOError

instance (MonadIO m, FromIOError a) => MonadIO (ErrT a m) where
    liftIO = ErrT . liftIO . liftM (left fromIOException)
                  . (try :: IO a -> IO (Either IOError a))

现在,如果我们将所有这些放在一个模块中并仅导出数据类型runErrT,而不导出构造函数ErrT,那么在ErrT内执行IO的所有内容都将正确排除异常已处理,因为IO操作只能通过liftIO引入。

如果需要,还可以将IOError替换为SomeException并处理所有异常。