2014-08-27 73 views
13

我有一個像快速檢查:如何使用全面性檢查,以防止和類型

data Mytype 
    = C1 
    | C2 Char 
    | C3 Int String 

如果我caseMytype一個Haskell數據類型而忘記處理的情況下,一個被遺忘的建設者,GHC給我警告(詳盡檢查)。

我現在想寫一個快速檢查Arbitrary實例產生MyTypes,如:

instance Arbitrary Mytype where 
    arbitrary = do 
    n <- choose (1, 3 :: Int) 
    case n of 
     1 -> C1 
     2 -> C2 <$> arbitrary 
     3 -> C3 <$> arbitrary <*> someCustomGen 

這樣做的問題是,我可以添加一個新的替代Mytype和忘記更新任意實例,從而有我測試不測試該替代方案。

我想找一種方法來使用GHC的詳盡檢查器來提醒我在我的任意實例中遺忘的案例。

我想出的最好的是

arbitrary = do 
    x <- elements [C1, C2 undefined, C3 undefined undefined] 
    case x of 
    C1  -> C1 
    C2 _ -> C2 <$> arbitrary 
    C3 _ _ -> C3 <$> arbitrary <*> someCustomGen 

但它並沒有真正感受到優雅。

我直覺地認爲沒有100%乾淨的解決方案,但是會希望減少忘記這種情況的機會 - 特別是在代碼和測試分開的大項目中。

+2

只是說明:可以寫'C2 {}'而不是'C2 _'等,這樣至少可以使語法更好一些。 – nh2 2014-08-28 19:59:52

+2

請注意,如果構造函數嚴格,undefined將失敗。 – 2014-09-04 08:23:02

+0

是否有某些原因,你不想只用TH自動導出任意實例? – 2014-09-04 18:52:02

回答

1

這裏我利用一個未使用的變量_x。不過,這並不比你的解決方案更優雅。

instance Arbitrary Mytype where 
    arbitrary = do 
    let _x = case _x of C1 -> _x ; C2 _ -> _x ; C3 _ _ -> _x 
    n <- choose (1, 3 :: Int) 
    case n of 
     1 -> C1 
     2 -> C2 <$> arbitrary 
     3 -> C3 <$> arbitrary <*> someCustomGen 

當然,人們必須保留最後case相干的_x虛擬定義,所以它不是完全乾燥。

另外,也可以利用Template Haskell來編譯編譯時斷言,檢查Data.Data.dataTypeOf中的構造函數是否是預期的構造函數。這個斷言必須與Arbitrary實例保持一致,所以這也不完全是DRY。如果你不需要自定義生成器,我相信Data.Data可以被利用來通過模板Haskell生成Arbitrary實例(我認爲我看到一些代碼正是這樣做,但我不記得在哪裏)。這樣,實例就不可能錯過構造函數。

+0

另一種可能性是使用'GHC.Generics'來派生任意實例。 'GHC.Generics'非常適用於可以爲Sums(數據類型的構造函數)和Products(數據構造函數的Fields)說明做什麼的情況,這個應該是這個實例的一個實例。 – bennofs 2014-08-28 19:37:26

+0

@bennofs不幸的是'GHC.Generics'只有在你想在所有字段中使用默認的'任意'時纔有幫助。在這種情況下,是的,它們對於這個目的非常好,但在我的情況下,我可以自定義實例很重要(我試圖通過包含'someCustomGen'來暗示)。 – nh2 2014-08-28 19:56:58

+0

你提出的自我遞歸方式肯定比我的'undefined'更好。 – nh2 2014-08-28 20:01:56

1

我用TemplateHaskell實現了一個解決方案,你可以在https://gist.github.com/nh2/d982e2ca4280a03364a8找到一個原型。有了這個,你可以這樣寫:

instance Arbitrary Mytype where 
    arbitrary = oneof $(exhaustivenessCheck ''Mytype [| 
     [ pure C1 
     , C2 <$> arbitrary 
     , C3 <$> arbitrary <*> arbitrary 
     ] 
    |]) 

它的工作原理是這樣的:你給它一個類型名稱(如''Mytype)和表達式(在我的情況下arbitrary風格Gen的List)。它獲取該類型名稱的所有構造函數的列表,並檢查該表達式是否至少包含所有這些構造函數。如果您只是添加了一個構造函數,但忘記將其添加到任意實例中,則此函數將在編譯時警告您。

這是它是如何與TH實現:

exhaustivenessCheck :: Name -> Q Exp -> Q Exp 
exhaustivenessCheck tyName qList = do 
    tyInfo <- reify tyName 
    let conNames = case tyInfo of 
     TyConI (DataD _cxt _name _tyVarBndrs cons _derives) -> map conNameOf cons 
     _ -> fail "exhaustivenessCheck: Can only handle simple data declarations" 

    list <- qList 
    case list of 
    [email protected](ListE l) -> do 
     -- We could be more specific by searching for `ConE`s in `l` 
     let cons = toListOf tinplate l :: [Name] 
     case filter (`notElem` cons) conNames of 
     [] -> return input 
     missings -> fail $ "exhaustivenessCheck: missing case: " ++ show missings 
    _ -> fail "exhaustivenessCheck: argument must be a list" 

我使用GHC.Generics輕鬆穿越Exp的語法樹:有了toListOf tinplate exp :: [Name](從lens)我可以很容易地找到所有Name S IN的全exp

我感到驚訝的是,從Language.Haskell.TH的類型沒有Generic情況下,既不(與目前GHC 7.8)做IntegerWord8 - 這些Generic實例,因爲他們出現在Exp需要。因此,我將它們添加爲孤立實例(對於大多數情況,StandaloneDeriving這樣做,但對於像Integer這樣的基本類型,我不得不復制粘貼實例,因爲它們具有它們)。

該解決方案並不完美,因爲它沒有使用像case這樣的詳盡性檢查器,但正如我們所認同的那樣,在DRY中這是不可能的,而這個TH解決方案是乾的。

一個可能的改進/替代方案是編寫一個TH函數,它檢查整個模塊中的所有任意實例,而不是在每個任意實例中調用exhaustivenessCheck

1

您希望確保您的代碼以特定方式運行;檢查代碼行爲的最簡單方法是測試它。

在這種情況下,所需的行爲是每個構造函數在測試中獲得合理的覆蓋率。我們可以通過一個簡單的測試來檢查:

allCons xs = length xs > 100 ==> length constructors == 3 
      where constructors = nubBy eqCons xs 
        eqCons C1  C1  = True 
        eqCons C1  _  = False 
        eqCons (C2 _) (C2 _) = True 
        eqCons (C2 _) _  = False 
        eqCons (C3 _ _) (C3 _ _) = True 
        eqCons (C3 _ _) _  = False 

這是非常天真的,但它是一個很好的第一槍。其優點:

  • eqCons會觸發全面性如果新構造的添加,這是你想要
  • 什麼它檢查您的實例處理所有構造函數,這是你想要
  • 亦是警告所有構造函數實際上與一些有用的概率產生(在這種情況下,至少1%)
  • 檢查您的實例是可用的,例如檢查。不掛

其缺點:

  • 需要大量的測試數據,以過濾掉那些長度> 100
  • eqCons是相當冗長的,因爲全部接收eqCons _ _ = False會繞過全面性檢查
  • 用途神奇數字100和3
  • 不是很通用

有一些方法可以改善這一點,例如。我們可以使用Data來計算構造函數。數據模塊:

allCons xs = sufficient ==> length constructors == consCount 
      where sufficient = length xs > 100 * consCount 
        constructors = length . nub . map toConstr $ xs 
        consCount = dataTypeConstrs (head xs) 

這失去編譯時檢查窮盡,但是我們經常測試它是多餘的,只要我們的代碼變得更通用。

如果我們真正想要的全面性檢查,有幾個地方,我們可以鞋拔它放回:

allCons xs = sufficient ==> length constructors == consCount 
      where sufficient = length xs > 100 * consCount 
        constructors = length . nub . map toConstr $ xs 
        consCount = length . dataTypeConstrs $ case head xs of 
                    [email protected](C1)  -> x 
                    [email protected](C2 _) -> x 
                    [email protected](C3 _ _) -> x 

注意,我們使用consCount完全消除魔法3。神奇的100(確定構造函數的最低要求頻率)現在與consCount一起縮放,但這隻需要更多的測試數據!

我們能夠解決很容易使用NEWTYPE:

consCount = length (dataTypeConstrs C1) 

newtype MyTypeList = MTL [MyType] deriving (Eq,Show) 

instance Arbitrary MyTypeList where 
    arbitrary = MTL <$> vectorOf (100 * consCount) arbitrary 
    shrink (MTL xs) = MTL (shrink <$> xs) 

allCons (MTL xs) = length constructors == consCount 
        where constructors = length . nub . map toConstr $ xs 

我們可以把一個簡單的全面性檢查中有沒有什麼地方,如果我們願意,例如。

instance Arbitrary MyTypeList where 
    arbitrary = do x <- arbitrary 
       MTL <$> vectorOf (100 * consCount) getT 
       where getT = do x <- arbitrary 
           return $ case x of 
              C1  -> x 
              C2 _ -> x 
              C3 _ _ -> x 
    shrink (MTL xs) = MTL (shrink <$> xs)