QuickCheck 中 Arbitrary 的唯一性和其他限制

Uniqueness and other restrictions for Arbitrary in QuickCheck

我正在尝试为我的数据类型编写修改后的 Arbitrary 实例,其中(在我的例子中)子组件的类型为 [String]。理想情况下,我希望在实例本身中带来唯一性,这样我就不需要 ==> headers / 我编写的每个测试的先决条件。

这是我的数据类型:

data Foo = Vars [String]

和琐碎的 Arbitrary 实例:

instance Arbitrary Foo where
  arbitrary = Vars <$> (:[]) <$> choose ('A','z')

这个例子很奇怪,我知道。过去,当 quickcheck 组合爆炸时,我遇到了困难,所以我想保持这些值较小。另一个请求 - 例如,我如何创建一个生成的字符串少于 4 个字符的实例?

所有这一切,从根本上需要(布尔)谓词来增加 Arbitrary 个实例。这可能吗?

suchThat :: Gen a -> (a -> Bool) -> Gen a 是一种在 Gen 中嵌入布尔谓词的方法。有关详细信息,请参阅 haddocks

以下是使实例唯一的方法:

instance Arbitrary Foo where
  arbitrary = Vars <$> (:[]) <$> (:[]) <$> choose ('A','z')
              `suchThat` isUnique
    where
      isUnique x = nub x == x

您肯定希望实例只生成符合数据类型意图的实例。如果您希望所有变量都不同,Arbitrary 实例必须反映这一点。 (另一个问题是,在这种情况下,将 Vars 定义为一个集合是否更有意义,例如 newtype Vars = Set [String]。)

我建议使用 SetHashtable 检查重复项,因为 nub 具有 O(n^2) 复杂度,对于较大的输入,这可能会大大降低您的测试速度。例如:

import Control.Applicative
import Data.List (nub)
import qualified Data.Set as Set
import Test.QuickCheck

newtype Foo = Vars [String]

-- | Checks if a given list has no duplicates in _O(n log n)_.
hasNoDups :: (Ord a) => [a] -> Bool
hasNoDups = loop Set.empty
  where
    loop _ []       = True
    loop s (x:xs) | s' <- Set.insert x s, Set.size s' > Set.size s
                    = loop s' xs
                  | otherwise
                    = False

-- | Always worth to test if we wrote `hasNoDups` properly.
prop_hasNoDups :: [Int] -> Property
prop_hasNoDups xs = hasNoDups xs === (nub xs == xs)

然后您的实例需要创建一个列表列表,每个列表应该是随机的。因此,您需要调用 listOf 两次,而不是 (: []),它只创建一个单例列表(并且只有一个级别):

instance Arbitrary Foo where
  arbitrary = Vars <$> (listOf . listOf $ choose ('A','z'))
                       `suchThat` hasNoDups

还要注意 choose ('A', 'z') 允许使用 Az 之间的所有字符,其中包括许多控制字符。我的猜测是你更想要像

这样的东西
oneof [choose ('A','Z'), choose ('a','z')]

如果你真的想要,你也可以使用 hash tables in the ST monad 使 hasNoDups O(n)


关于限制大小:您总是可以拥有自己的参数化函数来产生不同的 Gen Foo,但我想说在大多数情况下没有必要。 Gen 有它自己的内部大小参数,该参数在整个测试过程中都会增加(请参阅 this answer),因此涵盖了不同大小(使用 listOf 生成)的列表。

但我建议你实施 shrink,因为这会给你更好的反例。例如,如果我们定义(一个错误的测试)试图验证 Var 的任何实例在其任何变量中都不包含 'a'

prop_Foo_hasNoDups :: Foo -> Property
prop_Foo_hasNoDups (Vars xs) = all (notElem 'a') xs === True

我们会得到丑陋的反例,例如

Vars ["RhdaJytDWKm","FHHhrqbI","JVPKGTqNCN","awa","DABsOGNRYz","Wshubp","Iab","pl"]

但添加

shrink (Vars xs) = map Vars $ shrink xs

Arbitrary Foo使得反例只是

Vars ["a"]