我正在使用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处理这个问题的标准方法是什么。从目前为止的答案来看,似乎惯用的方法是抛出异常并将其处理得更高。看起来有点反直觉,有两个控制和返回路径流,但它显然是它的意图。
答案 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
我们一般会这样做,然后只获得你想要的信息。
您可以提取将try
与EitherT
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
您可以使用testEx :: EitherT String IO ()
testEx = message . anyException . tryIO $ fails
和try
将MonadError
内容与任何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 的来自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
以及相应的Applicative
,Monad
和MonadError
个实例。
现在让我们添加一种方法,将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
并处理所有异常。