【问题标题】:How to use SmallCheck in Haskell?如何在 Haskell 中使用 SmallCheck?
【发布时间】:2013-05-15 01:21:14
【问题描述】:

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

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

    通过将Control.Monad.liftM 明智地应用到makeTale 之类的功能,我设法使quickCheck 像这样工作而不会弄脏我的手。我想不出用smallCheck 来做这件事的方法(请给我解释一下!)。

  • SerialSeries等类型有什么关系?

  • (可选)coSeries 的意义何在?如何使用 SmallCheck.Series 中的 Positive 类型?

  • (可选)任何关于什么是单子表达式背后的逻辑的说明,以及在 smallCheck 的上下文中什么只是一个常规函数,将不胜感激。

如果有任何使用 smallCheck 的介绍/教程,我会很感激一个链接。非常感谢!

更新:我应该补充一点,我为smallCheck 找到的最有用和可读的文档是this paper (PDF)。乍一看,我找不到问题的答案;它更像是一个有说服力的广告而不是一个教程。

更新 2:我将关于在 Test.SmallCheck.list 和其他地方出现的奇怪 Identity 的问题移至 separate question

【问题讨论】:

    标签: testing haskell automated-tests monads smallcheck


    【解决方案1】:

    注意:此答案描述了 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 的 Arbitrary 类型类中的 coarbitrary 一样,它让我们构建了一系列“小”函数。请注意,我们在输入类型上编写实例——结果类型在另一个 Serial 参数中传递给我们(我在下面调用 results)。

    instance Serial Bool where
      coseries results d = [\cond -> if cond then r1 else r2 | 
                            r1 <- results d
                            r2 <- results d]
    

    这些需要更多的聪明才智来编写,我实际上会推荐您使用alts 方法,我将在下面简要描述。


    那么我们怎样才能制作一些SeriesPersons 呢?这部分很简单

    instance Series Person where
      series           d = SnowWhite : take (d-1) (map Dwarf [1..7])
      ...
    

    但是我们的coseries 函数需要生成从Persons 到其他东西的所有可能的函数。这可以使用 SmallCheck 提供的altsN 系列函数来完成。这是一种写法

     coseries results d = [\person -> 
                             case person of
                               SnowWhite -> f 0
                               Dwarf n   -> f n
                           | f <- alts1 results d ]
    

    基本思想是altsN results 生成SeriesN-ary 函数,从具有Serial 实例的N 值到ResultsSerial 实例。因此,我们使用它从 [0..7](先前定义的 Serial 值)到我们需要的任何值创建一个函数,然后我们将 Persons 映射到数字并将它们传入。


    现在我们有一个PersonSerial 实例,我们可以使用它来构建更复杂的嵌套Serial 实例。对于“实例”,如果FairyTalePersons 的列表,我们可以在Serial Person 实例旁边使用Serial a =&gt; Serial [a] 实例来轻松创建Serial FairyTale

    instance Serial FairyTale where
      series = map makeFairyTale . series
      coseries results = map (makeFairyTale .) . coseries results
    

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

    【讨论】:

    • 尊敬的电话,谢谢您的回答。我欣赏具体性;很清楚Series a 只是Depth -&gt; [a]。还有一个问题让我很困惑:为什么SmallCheck.Series.list的类型这么奇怪?我使用Identity时应该忽略它吗?
    • 呃 - 实际上我遇到了一些问题。我正在尝试编译SnowWhite 示例(即使Person 成为Serial 的实例)。我收到有关“种类不匹配”的错误;显然Serial 的论点应该有一种* -&gt; *Person 有一种*。我不知道如何解决这个问题。此外,根据在 ghc 中执行:i SeriesSeries 只是 Depth -&gt; [a] 似乎不是真的(尽管我知道它在道德上应该是什么非常有帮助)。也许您的解释仅对旧版本有效?谢谢!
    • 我已经部分解决了上述问题。如果您有兴趣,请参阅我写的“答案”以及从中链接的其他问题。再次感谢您,非常感谢,我现在对一切都理解得更好了。在一个合理的世界里,你的答案在鼻子上是正确的。另一方面,我相信有人会向我解释smallCheck 从其设计中的所有额外混乱中获得了什么好处。
    • 对不起,我实际上并没有测试所有的代码。它有很多错误。我现在无法在手机上修复它们:我稍后会。
    • 这些示例基于 Smallcheck 0.6。从最新版本 1.0.2 开始,Series 已从 Depth -&gt; [a] 更改为 MonadLogic 的实例。 Monad 逻辑基本上是一个列表,在层之间隐藏了一些 monadic 效果(它是未损坏的ListT 转换器的一个可能版本)。在这个新版本中,强烈建议使用类似于altsN 构造函数的consN 构造函数来构建您的Serial 实例。
    【解决方案2】:
    • 如果我有一个数据类型data Person = SnowWhite | Dwarf Integer,我该如何向smallCheck解释有效值是Dwarf 1Dwarf 7(或SnowWhite)?

    首先,您需要确定要为每个深度生成哪些值。这里没有一个正确的答案,这取决于您希望搜索空间有多细。

    这里只有两种可能的选择:

    1. people d = SnowWhite : map Dwarf [1..7](不依赖于深度)
    2. people d = take d $ SnowWhite : map Dwarf [1..7](每个深度单位增加一个元素的搜索空间)

    在你决定之后,你的 Serial 实例就这么简单

    instance Serial m Person where
        series = generate people
    

    我们在这里留下了 m 多态性,因为我们不需要底层 monad 的任何特定结构。

    • 如果我有一个复杂的FairyTale 数据结构和一个构造函数makeTale :: [Person] -&gt; FairyTale,并且我希望smallCheck 使用构造函数从Person-s 列表中制作FairyTale-s,该怎么办?

    使用cons1:

    instance Serial m FairyTale where
      series = cons1 makeTale
    
    • SerialSeries等类型有什么关系?

    Serial 是一个类型类; Series 是一种类型。您可以拥有多个相同类型的Series — 它们对应于枚举该类型值的不同方式。但是,为每个值指定如何生成它可能很困难。 Serial 类让我们可以为生成特定类型的值指定一个好的默认值。

    Serial的定义是

    class Monad m => Serial m a where
      series   :: Series m a
    

    所以它所做的只是将特定的Series m a 分配给ma 的给定组合。

    • coseries 的意义何在?

    需要生成函数类型的值。

    • 如何使用来自SmallCheck.SeriesPositive 类型?

    例如,像这样:

    > smallCheck 10 $ \n -> n^3 >= (n :: Integer)
    Failed test no. 5.
    there exists -2 such that
      condition is false
    
    > smallCheck 10 $ \(Positive n) -> n^3 >= (n :: Integer)
    Completed 10 tests without failure.
    
    • 任何关于什么是单子表达式背后的逻辑的说明,以及在 smallCheck 的上下文中什么只是一个常规函数,将不胜感激。

    当您编写 Serial 实例(或任何 Series 表达式)时,您在 Series m monad 中工作。

    在编写测试时,您使用返回 BoolProperty m 的简单函数。

    【讨论】:

      【解决方案3】:

      虽然我认为@tel 的回答是一个很好的解释(我希望smallCheck 实际上按照他描述的方式工作),但他提供的代码对我不起作用(smallCheck 版本1)。我设法让以下工作......

      更新/警告:下面的代码是错误的,原因很微妙。有关更正版本和详细信息,请参阅this answer 对下面提到的问题。简短的版本是必须写 instance (Monad m) =&gt; Series m Person 而不是 instance Serial Identity Person

      ...但我发现 Control.Monad.Identity 和所有编译器标志的使用很奇怪,我已经询问了 separate question

      还要注意,虽然Series Person(或实际上是Series Identity Person)实际上与函数Depth -&gt; [Person](参见@tel 的回答)并不完全相同,但函数generate :: Depth -&gt; [a] -&gt; Series m a 在它们之间进行转换。

      {-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, FlexibleContexts, UndecidableInstances #-}
      import Test.SmallCheck
      import Test.SmallCheck.Series
      import Control.Monad.Identity
      
      data Person = SnowWhite | Dwarf Int
      
      instance Serial Identity Person where
              series = generate (\d -> SnowWhite : take (d-1) (map Dwarf [1..7]))
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-11-02
        • 1970-01-01
        • 2017-03-09
        • 2015-09-20
        • 2017-09-17
        • 2021-05-24
        • 1970-01-01
        相关资源
        最近更新 更多