3

SO question 13350164 How do I test for an error in Haskell? に基づく 、無効な入力が与えられた場合、再帰関数が例外を発生させることをアサートする単体テストを作成しようとしています。私が採用したアプローチは、非再帰関数 (または最初の呼び出しで例外が発生した場合) にはうまく機能しますが、例外が呼び出しチェーンの奥深くで発生するとすぐに、アサーションは失敗します。

質問 6537766 Haskell のエラー処理へのアプローチに対する優れた回答を読みましたが、残念ながら、私の学習曲線のこの時点ではアドバイスが一般的すぎます。ここでの問題は、遅延評価と非純粋なテスト コードに関連していると思いますが、専門家の説明をいただければ幸いです。

Maybeこのような状況 (例:や) では、エラー処理に別のアプローチを採用するEither必要がありますか? または、このスタイルを使用しているときにテスト ケースを正しく動作させるための適切な修正方法はありますか?

これが私が思いついたコードです。最初の 2 つのテスト ケースは成功しますが、3 番目のテスト ケースは で失敗し"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。唯一の負の数がリストの最後のものである場合、コードは負の数のチェックを行いませ。2 番目のブランチにはチェックが含まれていません。

あなたのような再帰を書く標準的な方法は、このバグを回避するために「空」テストを破ることに注意する価値があります。一般に、ソリューションを「合計」と 2 つのガードに分解すると、エラーを回避するのに役立ちます。


ちなみに、エラー処理に対する Haskell のアプローチからの提案を支持します。Control.Exceptionは、推論して学習するのがはるかに難しく、error達成不可能なコード分岐をマークするためにのみ使用する必要がありますimpossible

提案を具体的にするために、 を使用してこの例を再構築できMaybeます。まず、unguarded 関数が組み込まれています。

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

次に、2 つのガード (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 (.)" (クライスリ アロー コンポジションとも呼ばれます) を使用して、次のように記述できます。sumPositiveInts

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

そして、コンビネータを使用してemptyIsNothingとの両方を単純化できますnegativeGivesNothing

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

emptyIsNothing = elseNothing null

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

含まれている値のいずれかが である場合、 wheresequence :: [Maybe a] -> Maybe [a]はリスト全体を失敗させますNothingsequence . map fは一般的なイディオムであるため、実際にはさらに一歩進めることができます

negativeGivesNothing = mapM (elseNothing (>= 0))

だから、結局

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