我正在构建一个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
的调用者不必设置两个不同的异常处理机制,而只能捕获一个异常类型的优点。
答案 0 :(得分:3)
您可以考虑EitherT
和errors
包。 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访问更加健壮的东西。