如何编写可任意修改输入的QuickCheck属性?——以isSubsequenceOf验证为例
嘿,我完全懂你的烦恼——QuickCheck的生成器组合有时候确实会因为类型约束或者依赖关系让人摸不着头脑,尤其是当你需要基于第一个生成的值来生成第二个的时候。咱们一步步拆解这个问题,先从你遇到的sublistOf类型问题说起,再给出两种可行的方案,包括你疑惑的monadic结构到底什么时候需要用。
为什么sublistOf会出现类型不匹配?
首先,sublistOf的类型是Arbitrary a => [a] -> Gen [a]——它接受一个列表,生成该列表的子列表(也就是通过移除原列表的某些元素得到的列表)。如果你尝试直接把它和顶层的Arbitrary实例结合,比如错误地写成:
prop_bad :: [Int] -> [Int] -> Property prop_bad xs ys = ys `isSubsequenceOf` xs
然后想用sublistOf xs来生成ys,就会出现类型不匹配——因为QuickCheck的顶层属性函数(比如quickCheck)要求参数是Arbitrary实例的类型,而Gen [a]并不是Arbitrary实例。
这时候你需要用forAll函数,它的类型是Gen a -> (a -> Property) -> Property,可以把一个生成器绑定到属性的参数上,解决类型问题。
方案1:验证子序列基本关系(用sublistOf,无需复杂monadic结构)
如果你想验证的是:如果ys是xs的子列表,那么ys一定是xs的子序列,直接用sublistOf结合forAll就能实现,写法非常简洁:
import Test.QuickCheck -- 先定义isSubsequenceOf的实现(如果标准库没有的话) isSubsequenceOf :: Eq a => [a] -> [a] -> Bool isSubsequenceOf [] _ = True isSubsequenceOf _ [] = False isSubsequenceOf (x:xs) (y:ys) | x == y = isSubsequenceOf xs ys | otherwise = isSubsequenceOf (x:xs) ys prop_subsequence_sublist :: [Int] -> Property prop_subsequence_sublist xs = forAll (sublistOf xs) $ \ys -> ys `isSubsequenceOf` xs
逻辑很清晰:先让QuickCheck生成任意的xs,然后用sublistOf xs生成xs的任意子列表ys,最后验证ys是xs的子序列。这个属性应该永远为True,因为子列表天然就是子序列。
方案2:生成超序列(依赖xs生成ys,需要monadic生成器)
如果你想验证反过来的场景:如果ys是xs的超序列(通过在xs中插入任意元素得到),那么xs一定是ys的子序列,这时候就需要基于xs生成ys,这时候就会用到monadic的Gen组合——但其实写起来并没有你想象的复杂。
方法A:灵活生成任意超序列
我们可以写一个生成器,先生成任意的“额外元素”列表,再把这些元素随机插入到xs中,得到ys:
-- 生成xs的任意超序列:在xs的任意位置插入任意元素 superSequenceOf :: Arbitrary a => [a] -> Gen [a] superSequenceOf xs = do extra <- listOf arbitrary -- 生成任意数量的额外元素 interleave xs extra -- 随机将extra的元素插入xs中 -- 辅助函数:随机交错合并两个列表 interleave :: [a] -> [a] -> Gen [a] interleave [] ys = pure ys interleave xs [] = pure xs interleave (x:xs) (y:ys) = do pickFirst <- arbitrary -- 生成Bool决定先取x还是y if pickFirst then (x :) <$> interleave xs (y:ys) else (y :) <$> interleave (x:xs) ys
然后用这个生成器编写属性:
prop_subsequence_supersequence :: [Int] -> Property prop_subsequence_supersequence xs = forAll (superSequenceOf xs) $ \ys -> xs `isSubsequenceOf` ys
这里的superSequenceOf用了do notation(monadic结构),但本质只是先生成额外元素,再随机插入到xs中——这正是QuickCheck的Gen monad设计的初衷:处理依赖型的生成逻辑。
方法B:极简超序列生成器
如果你不需要那么灵活的超序列,只想快速覆盖核心场景,可以用更简单的写法:
superSequenceOf' :: Arbitrary a => [a] -> Gen [a] superSequenceOf' xs = do prefix <- listOf arbitrary suffix <- listOf arbitrary pure $ prefix ++ xs ++ suffix
这个生成器生成的ys是前缀 + xs + 后缀,虽然生成的超序列种类有限,但能保证xs一定是ys的子序列,写法非常直观。
什么时候需要完整的monadic结构?
你疑惑是否必须编写完整的monadic结构,答案是:只有当你需要基于之前生成的值来生成下一个值时,才需要用monadic的Gen组合。比如生成超序列时,我们需要先得到xs,才能生成依赖于xs的ys,这时候就需要用do notation或者<$>/<*>等monadic操作符。
如果你的生成器不需要依赖之前的值(比如生成任意独立列表),直接用顶层的Arbitrary实例就可以了,完全不需要monadic结构。
内容的提问来源于stack exchange,提问作者Ignat Insarov

