2014-08-27 70 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)