Исключения и монадные трансформаторы
Я использую монадный трансформатор EitherT. Объединив его с монадой IO, я боюсь, что я получу исключение, и это не будет поймано.
Действительно, исключение просто проходит через:
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
в противном случае подходит для счета, чтобы передать вызывающим абонентам ошибку. Поэтому я хочу использовать это, а не исключать исключения...
Я посмотрел на try
из Control.Exception:
try :: Exception e => IO a -> IO (Either e a)
Кажется, именно то, что я хочу, это поместилось бы в мой стек Iither IO... (возможно, с добавленным hoistEither
и, возможно, fmapL
, и он начинает выглядеть многословным). Но наивный lift $ try
doesn 't typecheck.
Я уверен, что эта проблема была решена тысячи раз, но я не могу найти никакой хорошей ссылки, описывающей эту точную проблему. Как это должно быть разрешено?
РЕДАКТИРОВАТЬ. "Как это должно быть решено", меня интересовало идиоматическое решение, каким будет стандартный способ справиться с этим в haskell. Из ответов до сих пор кажется, что идиоматический способ состоит в том, чтобы исключить исключения и обрабатывать их выше. Похоже, что бит-интуитивен, чтобы иметь два потока управляющих и обратных путей, но это, по-видимому, способ, которым это должно было быть сделано.
Ответы
Ответ 1
Вы не хотите lift
try
выполнить вычисление, тогда вы получите Exception e => EitherT a IO (Either e ())
.
testEx :: (Exception e, MonadTrans m) => m IO (Either e ())
testEx = lift . try $ fails
Вам не нужна ошибка в результате, вы хотите интегрировать ошибку в EitherT
. Вы хотите интегрировать try
ing somethign с вашим EitherT
testEx :: (Exception e) => EitherT e IO ()
testEx = EitherT . try $ fails
Мы сделаем это в общем, а затем просто получим нужное сообщение.
Интегрируйте попытку с помощью EitherT
Вы можете извлечь идею интеграции 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
s; это то, что делает tryIO
из 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
Интегрируйте попытку с помощью MonadError
Вместо этого вы можете интегрировать 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
из предыдущего раздела
testEx :: EitherT String IO ()
testEx = message . anyException . tryIO $ fails
tryIO из Control.Error
tryIO
из Control.Error по существу является нашим первым tryIO
, за исключением того, что он только ловит IOException
вместо любого исключения. Фактически это определяется как
tryIO :: (MonadIO m) => IO a -> EitherT IOException m a
tryIO = EitherT . liftIO . try
Мы можем использовать его с message
для записи testEx
как
testEx :: EitherT String IO ()
testEx = message . tryIO $ fails
Ответ 2
На самом деле я думаю, что 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.
Ответ 3
Это еще один простой подход: пусть определит пользовательский монадный трансформатор точно так же, как 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
, все, что делает IO внутри ErrT
, будут иметь обработанные исключения, потому что IO
действия могут быть введены только через liftIO
.
Можно также заменить IOError
на SomeException
и обработать все исключения, если это необходимо.