给定一个任意树,我可以使用舒伯特编号在该树上构建一个子类型关系:
constructH :: Tree a -> Tree (Type a)
其中Type
嵌套原始标签,并另外提供执行子/父(或子类型)检查所需的数据。使用Schubert编号,两个Int参数就足够了。
data Type a where !Int -> !Int -> a -> Type a
这导致二元谓词
subtypeOf :: Type a -> Type a -> Bool
我现在想用QuickCheck测试这确实可以做我想做的事情。但是,以下属性不起作用,因为QuickCheck只是放弃了:
subtypeSanity ∷ Tree (Type ()) → Gen Prop
subtypeSanity Node { rootLabel = t, subForest = f } =
let subtypes = concatMap flatten f
in (not $ null subtypes) ==> conjoin
(forAll (elements subtypes) (\x → x `subtypeOf` t):(map subtypeSanity f))
如果我遗漏了对subtypeSanity
的递归调用,即我传递给conjoin
的列表的尾部,该属性运行正常,但只测试树的根节点!如果没有QuickCheck放弃生成新的测试用例,我如何递归地下载到我的数据结构中?
如果需要,我可以提供构建Schubert Hierarchy的代码,以及Arbitrary
的{{1}}实例,以提供完整的可运行示例,但这将是相当多的代码。我确信我只是没有“获得”QuickCheck,并且在这里以错误的方式使用它。
编辑:不幸的是,Tree (Type a)
功能似乎没有消除这里的问题。结果相同(见J. Abrahamson的回答。)
编辑II:我最终通过避免递归步骤来“修复”我的问题,并避免sized
。我们只列出树中的所有节点,然后测试那些单节点属性(从一开始就工作正常)。
conjoin
调整树的allNodes ∷ Tree a → [Tree a]
allNodes n@(Node { subForest = f }) = n:(concatMap allNodes f)
subtypeSanity ∷ Tree (Type ()) → Gen Prop
subtypeSanity tree = forAll (elements $ allNodes tree)
(\(Node { rootLabel = t, subForest = f }) →
let subtypes = concatMap flatten f
in (not $ null subtypes) ==> forAll (elements subtypes) (\x → x `subtypeOf` t))
实例不起作用。这是我仍在使用的任意实例:
Arbitrary
我认为问题“现在已经解决了”,但如果有人能向我解释为什么递归步骤和/或instance (Arbitrary a, Eq a) ⇒ Arbitrary (Tree (Type a)) where
arbitrary = liftM (constructH) $ sized arbTree
arbTree ∷ Arbitrary a ⇒ Int → Gen (Tree a)
arbTree n = do
m ← choose (0,n)
if m == 0
then Node <$> arbitrary <*> (return [])
else do part ← randomPartition n m
Node <$> arbitrary <*> mapM arbTree part
-- this is a crude way to find a sufficiently random x1,..,xm,
-- such that x1 + .. + xm = n, for any n, m, with 0 < m.
randomPartition ∷ Int → Int → Gen [Int]
randomPartition n m' = do
let m = m' - 1
seed ← liftM ((++[n]) . sort) $ replicateM m (choose (0,n))
return $ zipWith (-) seed (0:seed)
让QuickCheck放弃(在通过“仅”0测试之后),我会更多感激不尽。
答案 0 :(得分:7)
生成Arbitrary
递归结构时,QuickCheck通常有点太急切,并且会生成庞大的,随机的大量示例。这些是不合需要的,因为它们通常不能更好地检查感兴趣的性质并且可能非常慢。有两种解决方案
使用大小参数(sized
函数)和frequency
函数将生成器偏向小树。
使用类似于smallcheck
的小型导向生成器。这些尝试详尽地生成所有“小”示例,从而有助于保持树的大小。
为了阐明控制生成大小的sized
和frequency
方法,这里有一个示例RoseTree
data Rose a = It a | Rose [Rose a]
instance Arbitrary a => Arbitrary (Rose a) where
arbitrary = frequency
[ (3, It <$> arbitrary) -- The 3-to-1 ratio is chosen, ah,
-- arbitrarily...
-- you'll want to tune it
, (1, Rose <$> children)
]
where children = sized $ \n -> vectorOf n arbitrary
通过非常小心地控制子列表的大小,可以通过不同的Rose
形成更简单地完成
data Rose a = Rose a [Rose a]
instance Arbitrary a => Arbitrary (Rose a) where
arbitrary = Rose <$> arbitrary <*> sized (\n -> vectorOf (tuneUp n) arbitrary)
where tuneUp n = round $ fromIntegral n / 4.0
您可以在不引用sized
的情况下执行此操作,但这会为您的Arbitrary
实例的用户提供一个旋钮,以便在需要时请求更大的树。
答案 1 :(得分:2)
如果它对那些在这个问题上遇到困难的人有用:当QuickCheck“放弃”时,这表明你的先决条件(使用==>
)太难以满足。
QuickCheck使用简单的拒绝抽样技术:前置条件对值的生成有 no 效果。 QuickCheck生成一堆随机值,就像普通的一样。 生成之后,它们将通过前置条件发送:如果结果为True
,则使用该值测试属性;如果是False
,则丢弃该值。如果您的先决条件拒绝了QuickCheck生成的大部分值,那么QuickCheck将“放弃”(最好完全放弃,而不是通过统计上可疑的通过/失败声明)。
特别是,QuickCheck将不尝试生成满足给定前提条件的值。由您来确保您正在使用的生成器(arbitrary
或其他)生成许多通过前置条件的值。
让我们看一下你的例子中是如何表现的:
subtypeSanity :: Tree (Type ()) -> Gen Prop
subtypeSanity Node { rootLabel = t, subForest = f } =
let subtypes = concatMap flatten f
in (not $ null subtypes) ==> conjoin
(forAll (elements subtypes) (`subtypeOf` t):(map subtypeSanity f))
==>
只有一次出现,所以它的前提条件(not $ null subtypes
)必须太难以满足。这是由于递归调用map subtypeSanity f
:您不仅拒绝任何空Tree
的{{1}},还 (由于递归)拒绝subForest
包含Tree
个空subForest
s,和拒绝任何Tree
subForest
的{{1}} Tree
1}}包含subForest
个Tree
s,其中包含subForest
个空Tree
s,依此类推。
根据您的subForest
实例,arbitrary
仅嵌套到有限深度:最终我们将始终达到空Tree
,因此您的递归前置条件将始终失败,并且QuickCheck将放弃。