QuickCheck放弃调查递归数据结构(玫瑰树)。

时间:2013-12-03 13:40:20

标签: haskell recursion quickcheck

给定一个任意树,我可以使用舒伯特编号在该树上构建一个子类型关系:

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测试之后),我会更多感激不尽。

2 个答案:

答案 0 :(得分:7)

生成Arbitrary递归结构时,QuickCheck通常有点太急切,并且会生成庞大的,随机的大量示例。这些是不合需要的,因为它们通常不能更好地检查感兴趣的性质并且可能非常慢。有两种解决方案

  1. 使用大小参数(sized函数)和frequency函数将生成器偏向小树。

  2. 使用类似于smallcheck的小型导向生成器。这些尝试详尽地生成所有“小”示例,从而有助于保持树的大小。

  3. 为了阐明控制生成大小的sizedfrequency方法,这里有一个示例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}}包含subForestTree s,其中包含subForest个空Tree s,依此类推。

根据您的subForest实例,arbitrary仅嵌套到有限深度:最终我们将始终达到空Tree,因此您的递归前置条件将始终失败,并且QuickCheck将放弃。