Исключения и монадные трансформаторы

Я использую монадный трансформатор 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 и обработать все исключения, если это необходимо.