如何将IOError异常与本地相关异常相结合?

时间:2013-01-04 22:43:22

标签: haskell exception-handling

我正在构建一个Haskell应用程序并试图弄清楚我将如何构建错误处理机制。在实际的应用程序中,我正在与Mongo做一堆工作。但是,为此,我将通过在文件上使用基本IO操作来简化。

因此,对于这个测试应用程序,我想读取一个文件并验证它是否包含一个正确的fibonnacci序列,每个值用空格分隔:

1 1 2 3 5 8 13 21

现在,在阅读文件时,任何数量的内容实际上都可能出错,我将打电话给所有exceptions in the Haskell usage of the word

data FibException = FileUnreadable IOError
                  | FormatError String String
                  | InvalidValue Integer
                  | Unknown String

instance Error FibException where
    noMsg = Unknown "No error message"
    strMsg = Unknown

编写一个验证序列的纯函数,并在序列无效的情况下抛出错误很容易(虽然我可能做得更好):

verifySequence :: String -> (Integer, Integer) -> Either FibException ()
verifySequence "" (prev1, prev2) = return ()
verifySequence s (prev1, prev2) =
    let readInt = reads :: ReadS Integer
        res = readInt s in
    case res of
        [] -> throwError $ FormatError s
        (val, rest):[] -> case (prev1, prev2, val) of
            (0, 0, 1) -> verifySequence rest (0, 1)
            (p1, p2, val') -> (if p1 + p2 /= val'
                then throwError $ InvalidValue val'
                else verifySequence rest (p2, val))
            _ -> throwError $ InvalidValue val

之后,我想要读取文件的函数并验证序列:

type FibIOMonad = ErrorT FibException IO

verifyFibFile :: FilePath -> FibIOMonad ()
verifyFibFile path = do
    sequenceStr <- liftIO $ readFile path
    case (verifySequence sequenceStr (0, 0)) of
        Right res -> return res
        Left err -> throwError err

如果文件格式无效(返回Left (FormatError "something"))或文件的序列号不符合Left (InvalidValue 15)),则此函数完全符合我的要求。但如果指定的文件不存在,则会引发错误。

如何捕获readFile可能产生的IO错误,以便将其转换为FileUnreadable错误?

作为一个附带问题,这甚至是最好的方法吗?我看到verifyFibFile的调用者不必设置两个不同的异常处理机制,而只能捕获一个异常类型的优点。

3 个答案:

答案 0 :(得分:3)

您可以考虑EitherTerrors包。 http://hackage.haskell.org/packages/archive/errors/1.3.1/doc/html/Control-Error-Util.html有一个实用程序tryIO,用于在IOError中捕获EitherT,您可以使用fmapLT将错误值映射到自定义类型。

具体做法是:

type FibIOMonad = EitherT FibException IO

verifyFibFile :: FilePath -> FibIOMonad ()
verifyFibFile path = do
    sequenceStr <- fmapLT FileUnreadable (tryIO $ readFile path)
    hoistEither $ verifySequence sequenceStr (0, 0)

答案 1 :(得分:1)

@Savanni D'Gerinel:你走在正确的轨道上。让我们从 verifyFibFile 中提取您的错误捕获代码,使其更通用,并稍微修改它,以便它直接在ErrorT中工作:

catchError' :: ErrorT e IO a -> (IOError -> ErrorT e IO a) -> ErrorT e IO a
catchError' m f =
    ErrorT $ catchError (runErrorT m) (fmap runErrorT f)

verifyFibFile 现在可以写成:

verifyFibFile' :: FilePath -> FibIOMonad ()
verifyFibFile' path = do
    sequenceStr <- catchError' (liftIO $ readFile path) (throwError . FileUnReadable)
    ErrorT . return $ verifySequence sequenceStr' (0, 0) 

请注意我们在 catchError'中所做的工作。我们已经从ErrorT e IO a操作中剥离了ErrorT构造函数,并且还从错误处理函数的返回值中删除了,我知道我们之后可以通过再次将控制操作的结果包装在ErrorT中来重构它们。

事实证明这是一种常见模式,可以使用除ErrorT之外的monad变换器来完成。它可能会变得棘手(例如,如何使用ReaderT执行此操作?)。幸运的是,monad-control行李已为许多常见的变压器提供此功能。

monad-control中的类型签名起初可能看起来很吓人。首先看一个函数:control。它的类型为:

control :: MonadBaseControl b m => (RunInBase m b -> b (StM m a)) -> m a

b成为IO

让我们更具体
control :: MonadBaseControl IO m => (RunInBase m IO -> IO (StM m a)) -> m a

m 是构建在 IO 之上的monad堆栈。在您的情况下,它将是 ErrorT IO

RunInBase m IO是魔法函数的类型别名,它采用类型为m a的值并返回类型为IO *something*的值,某些为某些值复杂的魔法,它编码IO内部整个monad堆栈的状态,并允许您在“欺骗”仅接受IO值的控制操作后重建m a值。 control 为您提供该功能,并为您处理重建。

将此问题应用于您的问题,我们再次重写 verifyFibFile

import Control.Monad.Trans.Control (control)
import Control.Exception (catch)


verifyFibFile'' :: FilePath -> FibIOMonad ()
verifyFibFile'' path = do
    sequenceStr <- control $ \run -> catch (run . liftIO $ readFile path) 
                                           (run . throwError . FileUnreadable)     
    ErrorT . return $ verifySequence sequenceStr' (0, 0) 

请记住,这只适用于MonadBaseControl b m的正确实例存在的情况。

Here是对monad-control的一个很好的介绍。

答案 2 :(得分:0)

所以,这是我开发的答案。它的核心是将readFile包含在正确的catchError语句中,然后解除。

verifyFibFile :: FilePath -> FibIOMonad ()
verifyFibFile path = do
    contents <- liftIO $ catchError (readFile path >>= return . Right) (return . Left . FileUnreadable)
    case contents of
        Right sequenceStr' -> case (verifySequence sequenceStr' (0, 0)) of
            Right res -> return res
            Left err -> throwError err
        Left err -> throwError err

因此,verifyFibFile在此解决方案中得到了更多嵌套。

显然,

readFile path的类型为IO String。在此上下文中,catchError的类型为:

catchError :: IO String -> (IOError -> IO String) -> IO String

所以,我的策略是捕获错误并将其转到Either的左侧,并将成功值转到右侧,将我的数据类型更改为:

catchError :: IO (Either FibException String) -> (IOError -> IO (Either FibException String)) -> IO (Either FibException String)

我在第一个参数中执行此操作,只需将结果包装到Right中。我认为除非return . Right成功,否则我不会实际执行代码的readFile path分支。在要捕获的其他参数中,我从IOError开始,将其包装在Left中,然后将其返回到IO上下文中。之后,无论结果如何,我都会将IO值提升到FibIOMonad上下文。

我对代码变得更嵌套这一事实感到困扰。我有Left个值,并且所有这些Left值都会被抛出。我基本上处于Either上下文中,我认为Either类的Monad实现的好处之一就是Left值只会被传递通过绑定操作,并且不会执行该上下文中的其他代码。我希望对此有所阐述,或者看看如何从该函数中删除嵌套。

也许它不能。但是,调用者似乎可以反复调用verifyFibFile,并且第一次verifyFibFile返回错误时执行基本停止。这有效:

runTest = do
    res <- verifyFibFile "goodfib.txt"
    liftIO $ putStrLn "goodfib.txt"
    --liftIO $ printResult "goodfib.txt" res

    res <- verifyFibFile "invalidValue.txt"
    liftIO $ putStrLn "invalidValue.txt"

    res <- verifyFibFile "formatError.txt"
    liftIO $ putStrLn "formatError.txt"

Main> runErrorT $ runTest
goodfib.txt
Left (InvalidValue 17)

鉴于我创建的文件,invalidValue.txt和formatError.txt都会导致错误,但此函数会为我返回Left (InvalidValue ...)

没关系,但我仍觉得我错过了我的解决方案。而且我不知道我是否能够将其转化为使MongoDB访问更加健壮的东西。