Haskell函数检查多种不同条件并在失败时返回错误消息的好方法是什么?
在Python或类似语言中,它会很简单:
if failure_1:
return "test1 failed"
if failure_2:
return "test2 failed"
...
if failure_n:
return "testn failed"
do_computation
如果没有Haskell中任意嵌套的case / if语句,你怎么做?
编辑:某些测试条件可能需要IO,这会将任何测试结果放入IO monad中。我相信这会给许多解决方案带来麻烦。
答案 0 :(得分:12)
所以,你被困在IO
里面,你想要检查一堆没有嵌套if
的条件。我希望你能原谅我通过回答来解决Haskell中更常见的问题解决方法。
在抽象中考虑这需要如何表现。检查一个条件有两个结果之一:
可以递归查看检查多个条件;每次运行“函数的其余部分”时,它都会触及下一个条件,直到达到刚返回结果的最后一步。现在,作为解决问题的第一步,让我们使用该结构将事情分开 - 所以基本上,我们希望将一堆任意条件转化为可以组合成多条件函数的片段。我们可以对这些作品的性质作出什么结论?
1)每件都可以返回两种不同类型中的一种;错误消息,或下一步的结果。
2)每个部分必须决定是否运行下一步,所以当组合步骤时,我们需要给它代表下一步作为参数的函数。
3)由于每个部分都需要进行下一步,为了保持统一的结构,我们需要一种方法将最终的无条件步骤转换为看起来与条件步骤相同的步骤。
第一个要求显然表明我们希望我们的结果类似于Either String a
。现在我们需要一个组合函数来满足第二个要求,并且需要一个包装函数来适应第三个要求。此外,在组合步骤时,我们可能希望能够访问上一步中的数据(例如,验证两个不同的输入,然后检查它们是否相等),因此每个步骤都需要将上一步的结果作为参数。
那么,调用每个步骤err a
的类型作为简写,其他函数可能具有哪些类型?
combineSteps :: err a -> (a -> err b) -> err b
wrapFinalStep :: a -> err a
那么,那些类型的签名看起来很奇怪,不是吗?
这种“运行可以早期失败并带有错误消息的计算”的一般策略确实适用于monadic实现;实际上mtl package已经有一个。更重要的是,对于这种情况,它还有一个monad 转换器,这意味着您可以将错误monad结构添加到另一个monad - 例如IO
。
所以,我们可以导入模块,创建一个类型同义词,将IO
包裹在温暖的模糊ErrorT
中,然后离开:
import Control.Monad.Error
type EIO a = ErrorT String IO a
assert pred err = if pred then return () else throwError err
askUser prompt = do
liftIO $ putStr prompt
liftIO getLine
main :: IO (Either String ())
main = runErrorT test
test :: EIO ()
test = do
x1 <- askUser "Please enter anything but the number 5: "
assert (x1 /= "5") "Entered 5"
x2 <- askUser "Please enter a capital letter Z: "
assert (x2 == "Z") "Didn't enter Z"
x3 <- askUser "Please enter the same thing you entered for the first question: "
assert (x3 == x1) $ "Didn't enter " ++ x1
return () -- superfluous, here to make the final result more explicit
正如您所料,运行test
的结果是:Right ()
表示成功,Left String
表示失败,其中String
是相应的消息;如果assert
返回失败,则不会执行以下任何操作。
为了测试IO
操作的结果,您可能会发现编写类似于assert
的辅助函数最简单,而是使用IO Bool
的参数或其他方法。
另请注意,使用liftIO
将IO
操作转换为EIO
和runErrorT
中的值以运行EIO
操作并返回{{ 1}}值与总体结果。如果您想了解更多细节,可以阅读monad transformers。
答案 1 :(得分:5)
通常,模式匹配比许多if语句更好,并且检查错误条件也不例外:
func :: [Int] -> Either String Int
func [] = Left "Empty lists are bad"
func [x]
| x < 0 = Left "Negative? Really?"
| odd x = Left "Try an even number"
func xs = Right (length xs)
此函数返回错误消息或参数的长度。首先尝试错误情况,并且只有在它们都不匹配的情况下才会执行。
答案 2 :(得分:1)
我认为你不能在守卫中使用IO。
相反,你可以这样做:
myIoAction filename = foldr ($) [noFile, fileTooLarge, notOnlyFile] do_computation
where do_computation
= do {- do something -}
return (Right answer)
noFile success
= do {- find out whether file exists -}
if {- file exists -} then success else return (Left "no file!")
fileTooLarge success
= do {- find out size of file -}
if maxFileSize < fileSize then return (Left "file too large") else success
-- etc
答案 3 :(得分:1)
将other question作为预期的修改,你可以创建类似switch / case语句
select :: Monad m => [(m Bool, m a)] -> m a -> m a
select fallback [] = fallback
select fallback ((check, action) : others) = do
ok <- check
if ok then action else select fallback others
newfile :: FilePath -> IO Bool
newfile x = select
(return True)
[ (return $ length x <= 0, return False)
, (doesFileExist x, return False) ]
尽管可以很容易地写出这个特别的
newFile [] = return False
newFile fn = fmap not $ doesFileExist fn
答案 4 :(得分:-1)
使用警卫:
f z
| failure_1 = ...
| failure_2 = ...
| failure_3 = ...
| failure_4 = ...
| otherwise = do_computation