Haskell 快速检查:如何使用穷尽性检查器来防止忘记总和类型的构造函数
我有一个Haskell数据类型,比如Haskell 快速检查:如何使用穷尽性检查器来防止忘记总和类型的构造函数,haskell,quickcheck,Haskell,Quickcheck,我有一个Haskell数据类型,比如 data Mytype = C1 | C2 Char | C3 Int String 如果我在Mytype上执行case而忘记处理其中一个案例,GHC会向我发出警告(详尽性检查) 我现在想编写一个快速检查任意实例来生成MyTypes如下: instance Arbitrary Mytype where arbitrary = do n <- choose (1, 3 :: Int) case n of 1
data Mytype
= C1
| C2 Char
| C3 Int String
如果我在Mytype
上执行case
而忘记处理其中一个案例,GHC会向我发出警告(详尽性检查)
我现在想编写一个快速检查任意
实例来生成MyTypes
如下:
instance Arbitrary Mytype where
arbitrary = do
n <- choose (1, 3 :: Int)
case n of
1 -> C1
2 -> C2 <$> arbitrary
3 -> C3 <$> arbitrary <*> someCustomGen
但感觉不太优雅
我直觉上觉得没有100%干净的解决方案,但我希望能减少忘记此类情况的机会——特别是在代码和测试分离的大型项目中。这里我利用了一个未使用的变量
\ux
。不过,这并不比您的解决方案更优雅
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
实例任意Mytype,其中
任意的
设x=C1->ux的情形x;C2->x;C3 u->x
n C1
2->C2任意
3->C3任意自定义根
当然,最后一个案例必须与\ux
的虚拟定义保持一致,因此它不是完全干燥的
或者,可以利用模板Haskell构建编译时断言,检查Data.Data.dataTypeOf
中的构造函数是否是预期的构造函数。这个断言必须与任意的
实例保持一致,因此这也不是完全干涸的
如果您不需要自定义生成器,我相信Data.Data
可以通过Template Haskell被利用来生成任意的实例(我想我看到一些代码正是这样做的,但我不记得在哪里)。这样,实例就不会错过构造函数。我用TemplateHaskell实现了一个解决方案,您可以在中找到原型。有了这个,你可以写:
instance Arbitrary Mytype where
arbitrary = oneof $(exhaustivenessCheck ''Mytype [|
[ pure C1
, C2 <$> arbitrary
, C3 <$> arbitrary <*> arbitrary
]
|])
我正在使用GHC.Generics
轻松遍历Exp
的语法树:使用toListOf tinplate Exp::[Name]
(从lens
)我可以轻松找到整个Exp
中的所有Name
我感到惊讶的是,Language.Haskell.TH
中的类型没有Generic
实例,也没有Integer
或Word8
-Generic
实例,因为它们出现在Exp
中。因此,我将它们添加为孤立实例(对于大多数情况,StandaloneDeriving
会这样做,但对于Integer
等基本类型,我必须复制粘贴实例,因为Int
拥有它们)
这个解决方案并不完美,因为它不像案例那样使用穷尽性检查器,但正如我们所同意的,保持干燥是不可能的,而这个解决方案是干燥的
一种可能的改进/替代方法是编写一个TH函数,该函数一次检查整个模块中的所有任意实例,而不是在每个任意实例中调用ExtravenesCheck
。您希望确保代码以特定方式运行;检查代码行为的最简单方法是测试它
在这种情况下,期望的行为是每个构造函数在测试中获得合理的覆盖率。我们可以通过一个简单的测试来检查:
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
非常冗长,因为一个catch-alleqCons\uu=False
将绕过穷尽性检查
- 使用幻数100和3
- 不太一般
有一些方法可以改进这一点,例如,我们可以使用数据计算构造函数。数据模块:
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
请注意,我们使用conscont来完全消除魔法3
。魔术100
(它确定了构造函数所需的最小频率)现在可以根据conscont进行缩放,但这只需要更多的测试数据
我们可以使用一种新类型很容易地解决这个问题:
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
conscont=length(dataTypeConstrs C1)
newtype MyTypeList=MTL[MyType]派生(等式,显示)
实例任意MyTypeList,其中
任意=MTL向量(100*conscont)任意
收缩(MTL xs)=MTL(收缩xs)
allCons(MTL xs)=长度构造函数==conscont
其中构造函数=长度。核心。映射到constr$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)
实例任意MyTypeList,其中
任意=do x x
C3\uux->x
收缩(MTL xs)=MTL(收缩xs)
以下是使用库的解决方案:
genericArbitraryG
负责生成MyType
的每个构造函数。在这种情况下,我们使用uniform
来获得构造函数的均匀分布。使用customGens
我们定义Mytype
中的每个String
字段都是使用someCustomGen
生成的
有关更多示例,请参见。另一种可能性是使用GHC.Generics
派生任意实例GHC.Generics
非常适合于您可以对sum(数据类型的构造函数)和Produ执行什么操作的情况
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)
{-# 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