捕获EitherT中的异常并保留底层monad变换器

时间:2015-04-19 03:04:21

标签: haskell

基本上我想了解如何进行异常处理。我正在使用exceptionsMonadThrow / MonadCatch,这对我来说并没有多大意义。我把它分解成了我能做到的最小的例子。我很欣赏这是非常做作的。

首先我定义一个任意的Exception

data ReadLineException = ReadLineException deriving (Show,Typeable)
instance Exception ReadLineException

然后有一个函数向用户询问一个数字,并使用MonadWriter跟踪发生的事情。如果未输入数字,则会使用throwM中的Control.Monad.Catch引发异常。

ioTell :: (MonadWriter [String] m, MonadIO m, MonadThrow m) => m Int
ioTell = do
    tell ["Some opening string"]
    liftIO $ print "Enter a number: "
    x <- liftIO $ getLine
    let mx = maybeRead x :: Maybe Int
    case mx of Nothing -> do
                    tell ["Invalid entry was chosen: " ++ show x]
                    throwM ReadLineException
               (Just a) -> do
                    tell ["Number chosen was " ++ show a]
                    return a

接下来,我定义了一些方法来任意捕获任何异常(假设其余的计算可以继续而不管任何异常):

eitherCatch :: (Monad m, MonadIO m, MonadThrow m, MonadCatch m) => EitherT SomeException m a -> EitherT SomeException m a
eitherCatch et = catchAll et (\e -> left e)

在一般类型的情况下,我定义了一个带有转义函数的Transformer堆栈:

type MyExcept a = EitherT SomeException (WriterT [String] IO) a

runMyExcept :: MyExcept a -> IO (Either SomeException a,[String])
runMyExcept me = runWriterT $ runEitherT me

最后,我连续两次运行ioTell,收集两个作者的输出并合并它们(在我的实际用例中,这更复杂,因为我使用asyncwait)。

testM :: MyExcept Int
testM = eitherCatch $ do
    (ex,w1) <- liftIO $ runMyExcept ioTell
    let x = case ex of (Left _) -> 10
                       (Right a) -> a
    (ey,w2) <- liftIO $ runMyExcept ioTell
    let y = case ey of (Left _) -> 10
                       (Right a) -> a  
    tell $ w1 ++ w2 ++  ["Combined value is " ++ show (x + y)]
    return (x + y)

这没有发现异常。控制台输出:

"Enter a number: "
2
"Enter a number: "
x
(Left ReadLineException,[])

经过一些试验和错误,我发现在运行eitherCatch之前我可以通过运行runMyExcept来解决错误。这样,在尝试对模式进行模式匹配之前,异常会被抛入Left构造函数中(现在看起来很明显,尽管无法推断程序会纯粹从类型中逃脱):

testM :: MyExcept Int
testM = do
    (ex,w1) <- liftIO . runMyExcept $ eitherCatch ioTell
    let x = case ex of (Left _) -> 10
                       (Right a) -> a
    (ey,w2) <- liftIO . runMyExcept $ eitherCatch ioTell
    let y = case ey of (Left _) -> 10
                       (Right a) -> a  
    tell $ w1 ++ w2 ++  ["Combined value is " ++ show (x + y)]
    return (x + y)

第一个问题:这会丢失Writer由抛出异常的函数构建的状态:

"Enter a number: "
1
"Enter a number: "
x
(Right 11,["Some opening string","Number chosen was 1","Combined value is 11"])

第二个问题:这似乎也强制处理异常的地方。假设一个计算失败的那一刻我想让整个计算失败。我希望异常会通过调用堆栈扩散,并且可以在链中的任何位置捕获。

假设我可以找到第一个问题的解决方案,我认为我的唯一选择是tell一旦值进入并使用hoistEither来阻止其余的计算继续:

testM :: MyExcept Int
testM = do
    (ex,w1) <- liftIO . runMyExcept $ eitherCatch ioTell
    (ey,w2) <- liftIO . runMyExcept $ eitherCatch ioTell
    tell $ w1 ++ w2
    x <- hoistEither ex
    y <- hoistEither ey
    tell ["Combined value is " ++ show (x + y)]
    return (x + y)

然后hoistEither ex可以移动到后续调用ioTell以防止其执行,或者向下移动以允许两者都发生。

Finall,如果我将ioTell专门用于ioTell :: MyExcept Int并使用left代替throwM,那么所有这些都可行。我不必担心未经检查的异常,WriterT会被保留。对于手动生成异常的玩具示例,这很好。但总的来说呢?

0 个答案:

没有答案