我有一个像
这样的Haskell数据类型data Mytype
= C1
| C2 Char
| C3 Int String
如果我case
Mytype
并忘记处理其中一个案例,GHC会给我一个警告(详尽无遗检查)。
我现在想编写一个QuickCheck 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
添加一个新的替代方案而忘记更新Arbitrary实例,因此我的测试不会测试该替代实例。
我想找到一种方法,使用GHC的详尽检查器来提醒我在我的任意实例中遗忘的案例。
我提出的最好的是
arbitrary = do
x <- elements [C1, C2 undefined, C3 undefined undefined]
case x of
C1 -> C1
C2 _ -> C2 <$> arbitrary
C3 _ _ -> C3 <$> arbitrary <*> someCustomGen
但它并不是真的很优雅。
我直觉地认为没有100%清洁的解决方案,但是会欣赏任何可以减少忘记此类情况的机会 - 特别是在代码和测试分开的大型项目中。
答案 0 :(得分: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
实例保持一致,所以这也不是完全干的。
如果你不需要自定义生成器,我相信Data.Data
可以被利用来通过模板Haskell生成Arbitrary
实例(我想我看到一些代码正是这样做的,但我不记得在哪里)。通过这种方式,实例不可能错过构造函数。
答案 1 :(得分: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
的列表)。它获取该类型名称的所有构造函数的列表,并检查表达式是否至少包含所有这些构造函数一次。如果您刚添加了构造函数但忘记将其添加到Arbitrary实例,则此函数将在编译时向您发出警告。
这是用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
input@(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
)我可以轻松找到所有{{1}整个Name
中的s。
我很惊讶exp
中的类型没有Language.Haskell.TH
个实例,而且(当前GHC 7.8)都没有Generic
或Integer
- {{1}这些实例是必需的,因为它们出现在Word8
中。所以我将它们作为孤立实例添加(对于大多数情况,Generic
执行此操作但对于Exp
这样的原始类型,我必须复制粘贴实例,因为StandaloneDeriving
具有它们。)
解决方案并不完美,因为它没有像Integer
那样使用穷举检查器,但是正如我们所说,在保持DRY时这是不可能的,而这个TH解决方案是干的。
一种可能的改进/替代方法是编写一个TH函数,一次性检查整个模块中的所有Arbitrary实例,而不是在每个任意实例中调用Int
。
答案 2 :(得分: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
将触发详尽的警告,这就是你想要的它的缺点:
eqCons
非常详细,因为全能eqCons _ _ = False
会绕过穷举检查有很多方法可以改善这一点,例如。我们可以使用Data.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
x@(C1) -> x
x@(C2 _) -> x
x@(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)
答案 3 :(得分:0)
以下是使用generic-random
库的解决方案:
{-# language DeriveGeneric #-}
{-# language TypeOperators #-}
import Generic.Random
import GHC.Generics
import Test.QuickCheck
data Mytype
= C1
| C2 Char
| C3 Int String
deriving Generic
instance Arbitrary Mytype where
arbitrary = genericArbitraryG customGens uniform
where
customGens :: Gen String :+ ()
customGens = someCustomGen :+ ()
someCustomGen :: Gen String
someCustomGen = undefined
genericArbitraryG
负责生成MyType
的每个构造函数。在这种情况下,我们使用uniform
获得构造函数的均匀分布。使用customGens
,我们定义String
中的每个Mytype
字段都是用someCustomGen
生成的。
有关更多示例,请参见Generic.Random.Tutorial
。