简体   繁体   English

Haskell异常和单元测试

[英]Haskell exceptions and unit testing

Based on SO question 13350164 How do I test for an error in Haskell? 基于SO问题13350164 如何在Haskell中测试错误? , I'm trying to write a unit test which asserts that given invalid input, a recursive function raises an exception. ,我正在尝试编写一个单元测试,断言给定无效输入,递归函数引发异常。 The approach I adopted works well for non-recursive functions (or when the first call raises the exception), but as soon as the exception occurs deeper in the call chain, the assertion fails. 我采用的方法适用于非递归函数(或者当第一个调用引发异常时),但是一旦异常发生在调用链中,断言就会失败。

I've read the excellent answers to question 6537766 Haskell approaches to error handling but unfortunately the advice is a bit too generic for this point of my learning curve. 我已经阅读了问题6537766 Haskell错误处理方法的优秀答案,但不幸的是,对于我的学习曲线的这一点,建议有点过于通用。 My guess is that the problem here is connected to lazy evaluation and non-pure testing code, but I'd appreciate an expert explanation. 我的猜测是这里的问题与懒惰评估和非纯测试代码有关,但我很欣赏专家的解释。

Should I take a different approach to error handling in situations like this (eg Maybe or Either ), or is there a reasonable fix for making the test case work correctly while using this style? 我应该采取不同的方法来处理这种情况下的错误处理(例如, MaybeEither ),还是在使用这种风格时是否有合理的解决办法使测试用例正常工作?

Here's the code I've come up with. 这是我提出的代码。 The first two test cases succeed, but the third one fails with "Received no exception, but was expecting exception: Negative item" . 前两个测试用例成功,但第三个测试用例失败, "Received no exception, but was expecting exception: Negative item"

import Control.Exception (ErrorCall(ErrorCall), evaluate)
import Test.HUnit.Base  ((~?=), Test(TestCase, TestList))
import Test.HUnit.Text (runTestTT)
import Test.HUnit.Tools (assertRaises)

sumPositiveInts :: [Int] -> Int
sumPositiveInts [] = error "Empty list"
sumPositiveInts (x:[]) = x
sumPositiveInts (x:xs) | x >= 0 = x + sumPositiveInts xs
                       | otherwise = error "Negative item"

instance Eq ErrorCall where
    x == y = (show x) == (show y)

assertError msg ex f = 
    TestCase $ assertRaises msg (ErrorCall ex) $ evaluate f

tests = TestList [
  assertError "Empty" "Empty list" (sumPositiveInts ([]))
  , assertError "Negative head" "Negative item" (sumPositiveInts ([-1, -1]))
  , assertError "Negative second item" "Negative item" (sumPositiveInts ([1, -1]))
  ]   

main = runTestTT tests

It's actually just an error in sumPositiveInts . 它实际上只是sumPositiveInts一个错误。 Your code does not do negativity checking when the only negative number is the last one in the list—the second branch doesn't include the check. 当只有负数是最后一个列表,第二支不包括检查你的代码没有消极检查。

It's worth noting that the canonical way of writing recursion like yours would break the "emptiness" test out in order to avoid this bug. 值得注意的是,像你这样编写递归的规范方法会打破“空虚”测试,以避免这个错误。 Generally, decomposing your solution into "sum" plus two guards will help to avoid errors. 通常,将您的解决方案分解为“总和”加上两个警卫将有助于避免错误。


I second the suggestion from Haskell approaches to error handling by the way. 我是第二个Haskell建议的方法来处理错误 Control.Exception is much more difficult to reason about and learn and error should only be used to mark code branches which are impossible to achieve—I rather like some suggestions that it ought to have been called impossible . Control.Exception更难以推理和学习,并且error只应用于标记无法实现的代码分支 - 我更喜欢一些应该被称为impossible

To make the suggestion tangible, we can rebuild this example using Maybe . 为了使建议有形,我们可以使用Maybe重建这个例子。 First, the unguarded function is built in: 首先,无人看守的功能是内置的:

sum :: Num a => [a] -> a

then we need to build the two guards (1) empty lists give Nothing and (2) lists containing negative numbers give Nothing . 然后我们需要建立两个守卫(1)空列表给出Nothing和(2)包含负数的列表给出Nothing

emptyIsNothing :: [a] -> Maybe [a]
emptyIsNothing [] = Nothing
emptyIsNothing as = Just as

negativeGivesNothing :: [a] -> Maybe [a]
negativeGivesNothing xs | all (>= 0) xs = Just xs
                        | otherwise     = Nothing

and we can combine them as a monad 我们可以将它们组合成一个单子

sumPositiveInts :: [a] -> Maybe a
sumPositiveInts xs = do xs1 <- emptyIsNothing xs
                        xs2 <- negativeGivesNothing xs1
                        return (sum xs2)

And then there are lots of idioms and reductions we can employ to make this code much easier to read and write (once you know the conventions!). 然后我们可以使用许多习语和缩减来使这些代码更容易阅读和编写(一旦你知道约定!)。 Let me stress that nothing after this point is necessary nor terribly easy to understand. 让我强调, 在这一点之后没有必要,也不是非常容易理解。 Learning it improves your ability to decompose functions and fluently think about FP, but I'm just jumping to the advanced stuff. 学习它可以提高你分解函数的能力,并且流利地考虑FP,但我只是跳到高级的东西。

For instance, we can use "Monadic (.) " (which is also called Kleisli arrow composition) to write sumPositiveInts 例如,我们可以使用“Monadic (.) ”(也称为Kleisli箭头组合)来编写sumPositiveInts

sumPositiveInts :: [a] -> Maybe a
sumPositiveInts = emptyIsNothing >=> negativeGivesNothing >=> (return . sum)

and we can simplify both emptyIsNothing and negativeGivesNothing using a combinator 我们可以使用组合器简化emptyIsNothingnegativeGivesNothing

elseNothing :: (a -> Bool) -> a -> Just a
pred `elseNothing` x | pred x    = Just x
                     | otherwise = Nothing

emptyIsNothing = elseNothing null

negativeGivesNothing = sequence . map (elseNothing (>= 0))

where sequence :: [Maybe a] -> Maybe [a] fails an entire list if any of the contained values are Nothing . 其中sequence :: [Maybe a] -> Maybe [a]如果任何包含的值为Nothingsequence :: [Maybe a] -> Maybe [a]失败整个列表。 We can actually go one step further since sequence . map f 实际上,我们可以从sequence . map f更进一步sequence . map f sequence . map f is a common idiom sequence . map f是一种常见的习语

negativeGivesNothing = mapM (elseNothing (>= 0))

So, in the end 所以,最后

sumPositives :: [a] -> Maybe a
sumPositives = elseNothing null 
               >=> mapM (elseNothing (>= 0))
               >=> return . sum

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM