2013-02-10 131 views
2

基於SO問題13350164 How do I test for an error in Haskell?,我試圖編寫一個單元測試,它聲明給定無效輸入,遞歸函數引發異常。我採用的方法適用於非遞歸函數(或者當第一次調用引發異常時),但只要異常發生在調用鏈中更深處,斷言就會失敗。Haskell異常和單元測試

我讀過問題6537766 Haskell approaches to error handling的優秀答案,但不幸的是,對於我的學習曲線來說,這個建議有點過於通用。我的猜測是,這裏的問題與惰性評估和非純測試代碼相關,但我會很感激專家的解釋。

在這種情況下(例如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 

回答

6

這實際上只是在sumPositiveInts錯誤。你的代碼確實是而不是當唯一的負數是列表中的最後一個時 - 做第二個分支不包括檢查時做否定性檢查。

值得注意的是,像您的那樣編寫遞歸的規範方式會打破「空洞」測試,以避免此錯誤。通常,將解決方案分解爲「總和」加上兩個警衛將有助於避免錯誤。


順便提一下Haskell approaches to error handling的建議。 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) 

,我們可以使用一個組合子

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

emptyIsNothing = elseNothing null 

negativeGivesNothing = sequence . map (elseNothing (>= 0)) 
簡化既 emptyIsNothingnegativeGivesNothing

其中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);守護$ all(> 0)xs; return(sum xs)}'imo。 – dave4420 2013-02-10 22:47:46

+0

是的,我原本打算這麼做 - 我一直使用上面的「guard」這個詞。在我的天真單子代碼中看到「管道」後,我被激勵把它寫成一個Kleisli組合,但它絕對沒有價值。 – 2013-02-10 23:02:42