Haskell 异常和单元测试

3
基于stackoverflow上的问题13350164 如何在Haskell中测试错误?,我试图编写一个单元测试来断言如果给定无效输入,递归函数会引发异常。我采用的方法对于非递归函数(或者第一次调用引发异常时)效果很好,但是一旦异常在调用链深处发生,断言就会失败。
我已经阅读了问题6537766 Haskell处理错误的方法 的优秀答案,但不幸的是,该建议对于我当前的学习曲线来说有点太泛泛了。我的猜测是这里的问题与惰性求值和非纯测试代码有关,但我希望能得到专家的解释。
在这种情况下,我应该采用不同的错误处理方法(例如MaybeEither),还是有合理的修复方法可以使测试用例在使用此风格时正确工作?
以下是我想出的代码。前两个测试用例成功了,但第三个测试用例失败并显示"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
1个回答

7
实际上,这只是在sumPositiveInts中的一个错误。当列表中仅有一项为负数时,你的代码没有进行负数检查-第二个分支没有包括检查。值得注意的是,像您这样编写递归的规范方式会分解出“ sum”加两个保护措施,以避免出现错误。
顺便说一句,我赞成Haskell处理错误的方法的建议。 Control.Exception更难理解和学习,而且error应该仅用于标记不可能实现的代码分支-我很喜欢某些建议,它应该被称为impossible
为了使建议具体化,我们可以使用Maybe重构此示例。首先,构建未受保护的函数:
sum :: Num a => [a] -> a

然后我们需要构建两个守卫(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

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

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

接下来,我们可以采用许多成语和简化方式,使得这段代码更易于阅读和编写(一旦你了解了这些约定!)。请注意,在此之后的任何内容都不是必需的,也不是非常容易理解的。学习它可以提高你分解函数和流畅思考FP的能力,但我只是跳到了高级内容。

例如,我们可以使用“单子函子 (.)”(也称为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

2
也许Maybe是一个MonadPlus!在我看来,更清晰的写法是sumPositives xs = do { guard $ not (null xs) ; guard $ all (>0) xs ; return (sum xs) } - dave4420
是的,我最初打算这样做 - 关于我在上面使用“guard”一词的所有时间。在看到我天真的单子代码中的“管道”后,我受到了写作为Kleisli组合的动力,但这绝对是不必要的。 - J. Abrahamson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接