4

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

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

1 1 2 3 5 8 13 21

现在,在读取文件时,实际上可能有很多事情是错误的,我将在 Haskell 对单词的用法中调用所有这些异常

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不必设置两种不同的异常处理机制而可以只捕获一种异常类型的优点。

4

3 回答 3

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)
于 2013-01-05T20:13:34.070 回答
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让我们通过be使其更具体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 堆栈的状态,并让你在m a之后重建值,一旦你有“愚弄”只接受 IO 值的控制操作。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存在适当的实例时才有效。

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

于 2013-01-05T16:32:59.737 回答
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显然有 type 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的实现的好处之一是值将简单地通过绑定操作传递,并且不会执行该上下文中的其他代码。我很想对此进行一些说明,或者看看如何从这个函数中删除嵌套。MonadLeft

也许不能。然而,调用者似乎可以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 访问更加健壮的东西。

于 2013-01-05T03:45:41.523 回答