3

基于 SO 问题 13350164如何测试 Haskell 中的错误?,我正在尝试编写一个单元测试,它断言给定无效输入,递归函数会引发异常。我采用的方法适用于非递归函数(或者当第一次调用引发异常时),但是一旦异常发生在调用链的更深处,断言就会失败。

我已经阅读了问题 6537766 Haskell 错误处理方法的优秀答案,但不幸的是,对于我的学习曲线的这一点来说,这个建议有点太笼统了。我的猜测是这里的问题与惰性评估和非纯测试代码有关,但我希望能得到专家的解释。

Maybe在这种情况下(例如或),我是否应该采用不同的方法来处理错误Either,或者是否有合理的修复方法可以在使用这种风格时使测试用例正常工作?

这是我想出的代码。前两个测试用例成功,但第三个测试用例失败"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
4

1 回答 1

7

这实际上只是一个错误sumPositiveInts。当唯一的负数是列表中的最后一个时,您的代码不会进行负数检查——第二个分支不包括检查。

值得注意的是,像你这样编写递归的规范方法会打破“空虚”测试以避免这个错误。通常,将您的解决方案分解为“总和”加上两个守卫将有助于避免错误。


顺便说一句,我支持 Haskell 方法的错误处理建议。Control.Exception更难推理和学习,error只能用于标记无法实现的代码分支——我更喜欢一些建议,它应该被称为impossible.

为了使建议切实可行,我们可以使用Maybe. 首先,内置了 unguarded 函数:

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

然后我们需要构建两个守卫(1)空列表 giveNothing和(2)包含负数的列表 give Nothing

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

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

我们可以将它们组合成一个单子

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

然后,我们可以使用许多习语和简化来使这段代码更容易阅读和编写(一旦你知道了约定!)。让我强调一下,在这点之后,没有什么是必要的,也不是非常容易理解的。学习它可以提高你分解函数和流畅思考 FP 的能力,但我只是跳到高级的东西。

例如,我们可以使用“Monadic (.)”(也称为 Kleisli 箭头组合)来编写sumPositiveInts

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

我们可以简化两者emptyIsNothingnegativeGivesNothing使用组合器

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

emptyIsNothing = elseNothing null

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

如果sequence :: [Maybe a] -> Maybe [a]任何包含的值是Nothing. 我们实际上可以更进一步,因为这sequence . map f是一个常见的成语

negativeGivesNothing = mapM (elseNothing (>= 0))

所以,最后

sumPositives :: [a] -> Maybe a
sumPositives = elseNothing null 
               >=> mapM (elseNothing (>= 0))
               >=> return . sum
于 2013-02-10T21:30:43.410 回答