我正在尝试使用SmallCheck测试Haskell程序,但是我无法理解如何使用该库来测试自己的数据类型。显然,我需要使用Test.SmallCheck.Series。但是,我发现它的文档非常混乱。我对食谱风格的解决方案以及对逻辑(monadic?)结构的可理解的解释都感兴趣。这是我有的一些问题(所有相关问题):

  • 如果我的数据类型为data Person = SnowWhite | Dwarf Integer,如何通过smallCheck(或Dwarf 1)向Dwarf 7解释有效值为SnowWhite?如果我具有复杂的FairyTale数据结构和构造函数makeTale :: [Person] -> FairyTale,并且我希望smallCheck使用构造函数从Person-s列表中生成FairyTale-s怎么办?

    通过将明智的quickCheck应用程序用于Control.Monad.liftM之类的功能,我设法使makeTale像这样工作,而不会弄脏我的双手。我无法找到一种用smallCheck做到这一点的方法(请向我解释!)。
  • SerialSeries等类型之间的关系是什么?
  • (可选)coSeries的意义是什么?如何使用Positive中的SmallCheck.Series类型?
  • (可选)在smallCheck上下文中,对什么应该是一元表达式以及什么是正则函数进行逻辑解释的任何内容都将受到赞赏。

  • 如果有使用smallCheck的简介/教程,我希望您能找到一个链接。非常感谢你!

    更新:我应该补充一点,我发现smallCheck最有用,最易读的文档是this paper (PDF)。乍看之下,我找不到问题的答案;它比教程更有说服力。

    更新2:我将有关Identity和其他位置类型的奇怪Test.SmallCheck.list的问题移到separate question

    最佳答案

    注意:该答案描述了SmallCheck 1.0之前的版本。有关SmallCheck 0.6和1.0之间的重要区别,请参见this blog post

    SmallCheck就像QuickCheck一样,它在可能类型的一部分空间上测试属性。不同之处在于,它试图穷举枚举一系列所有“小”值,而不是枚举任何小值。

    正如我所暗示的,SmallCheck的Serial类似于QuickCheck的Arbitrary

    现在Serial非常简单:Serial类型a有一种方法(series)生成Series类型,这只是Depth -> [a]的功能。或者,为了解压缩,Serial对象是我们知道如何枚举其“较小”值的对象。我们还给了Depth参数,该参数控制我们应该生成多少个小值,但让我们暂时忽略它。

    instance Serial Bool where series _ = [False, True]
    instance Serial Char where series _ = "abcdefghijklmnopqrstuvwxyz"
    instance Serial a => Serial (Maybe a) where
      series d = Nothing : map Just (series d)
    

    在这些情况下,我们要做的只是忽略Depth参数,然后枚举每种类型的“所有”可能值。我们甚至可以针对某些类型自动执行此操作
    instance (Enum a, Bounded a) => Serial a where series _ = [minBound .. maxBound]
    

    这是一种彻底测试属性的非常简单的方法-从字面上测试每个可能的输入!显然,至少存在两个主要陷阱:(1)无限数据类型将在测试时导致无限循环,并且(2)嵌套类型导致要查看的示例空间成倍增加。在这两种情况下,SmallCheck都会很快变得非常大。

    这就是Depth参数的重点-它使系统要求我们将Series保持较小。根据文档,Depth

    生成测试值的最大深度

    对于数据值,它是嵌套构造函数应用程序的深度。

    对于功能值,既是嵌套案例分析的深度,也是结果的深度。

    因此,让我们重新设计示例以使其更小。
    instance Serial Bool where
      series 0 = []
      series 1 = [False]
      series _ = [False, True]
    instance Serial Char where
      series d = take d "abcdefghijklmnopqrstuvwxyz"
    instance Serial a => Serial (Maybe a) where
      -- we shrink d by one since we're adding Nothing
      series d = Nothing : map Just (series (d-1))
    
    instance (Enum a, Bounded a) => Serial a where series d = take d [minBound .. maxBound]
    

    好多了。

    那么coseries是什么?就像QuickCheck的coarbitrary类型类中的Arbitrary一样,它使我们可以构建一系列“小”功能。请注意,我们正在通过输入类型编写实例-结果类型将通过另一个Serial参数传递给我们(我在下面称为results)。
    instance Serial Bool where
      coseries results d = [\cond -> if cond then r1 else r2 |
                            r1 <- results d
                            r2 <- results d]
    

    这些编写起来需要更多的技巧,实际上我将指导您使用alts方法,下面将对其进行简要介绍。

    那么我们如何制作SeriesPerson?这部分很容易
    instance Series Person where
      series           d = SnowWhite : take (d-1) (map Dwarf [1..7])
      ...
    

    但是我们的coseries函数需要生成从Person到其他内容的所有可能函数。这可以使用SmallCheck提供的altsN系列功能来完成。这是一种写法
     coseries results d = [\person ->
                             case person of
                               SnowWhite -> f 0
                               Dwarf n   -> f n
                           | f <- alts1 results d ]
    

    基本思想是altsN results从具有Series实例的N值到NSerial实例生成Serial -ary函数的Results。因此,我们使用它来创建一个从[0..7](先前定义的Serial值)到所需函数的函数,然后将Person映射到数字并将其传递给他们。

    因此,现在我们有了SerialPerson实例,我们可以使用它来构建更复杂的嵌套Serial实例。对于“实例”,如果FairyTalePerson的列表,我们可以将Serial a => Serial [a]实例与Serial Person实例一起使用以轻松创建Serial FairyTale:
    instance Serial FairyTale where
      series = map makeFairyTale . series
      coseries results = map (makeFairyTale .) . coseries results
    

    ((makeFairyTale .)makeFairyTale生成的每个函数组成coseries,这有点令人困惑)

    09-05 04:08