基本上我想了解如何进行异常处理。我正在使用exceptions
包MonadThrow
/ 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
,收集两个作者的输出并合并它们(在我的实际用例中,这更复杂,因为我使用async
和wait
)。
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
会被保留。对于手动生成异常的玩具示例,这很好。但总的来说呢?