我正在尝试使用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,该怎么办?
我设法使quickCheck
像这样工作,而不会因为Control.Monad.liftM
等makeTale
这样的功能使用smallCheck
的明智应用而弄得太脏。我无法通过Serial
找到解决方法(请向我解释一下!)。
Series
,coSeries
等类型之间的关系是什么?
(可选)Positive
有什么意义?如何使用SmallCheck.Series
中的smallCheck
类型?
(可选)在smallCheck的上下文中,任何阐明应该是monadic表达式的逻辑是什么,以及什么只是常规函数,将不胜感激。
如果有任何使用smallCheck
的介绍/教程,我会很感激。非常感谢你!
更新:我应该补充一点,我在Identity
找到的最实用,最易读的文档是this paper (PDF)。第一眼看我在那里找不到我的问题的答案;它更像是一个有说服力的广告,而不是一个教程。
更新2 :我将有关奇怪的Test.SmallCheck.list
的问题移到了{{1}}以及其他地方的separate question。
答案 0 :(得分:17)
注意:此答案描述了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
方法,我将在下面简要介绍。
那么我们怎样才能制作Series
个Person
个?这部分很简单
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
个N
个Serial
值Serial
个Results
个实例到Serial
Person
的实例。因此我们使用它来创建一个函数,从[0..7],一个先前定义的Serial
值,到我们需要的任何值,然后我们将Person
映射到数字并传入'em。 / p>
现在我们有Serial
的{{1}}实例,我们可以使用它来构建更复杂的嵌套FairyTale
实例。对于“实例”,如果Person
是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
生成每个函数{{1}},这有点令人困惑)
答案 1 :(得分:4)
data Person = SnowWhite | Dwarf Integer
,如何向smallCheck
解释有效值为Dwarf 1
到Dwarf 7
(或SnowWhite
)? 首先,您需要确定要为每个深度生成哪些值。这里没有单一的正确答案,这取决于您希望搜索空间的细化程度。
以下是两种可能的选择:
people d = SnowWhite : map Dwarf [1..7]
(不依赖于深度)people d = take d $ SnowWhite : map Dwarf [1..7]
(每个深度单位将搜索空间增加一个元素)在您决定之后,您的Serial
实例就像
instance Serial m Person where
series = generate people
我们在这里留下了m
多态,因为我们不需要底层monad的任何特定结构。
FairyTale
数据结构和一个构造函数makeTale :: [Person] -> FairyTale
怎么办?我希望smallCheck
使用构造函数从Person-s列表中创建FairyTale?使用cons1
:
instance Serial m FairyTale where
series = cons1 makeTale
Serial
,Series
等之间的关系是什么? Serial
是一个类型类; Series
是一种类型。您可以拥有相同类型的多个Series
- 它们对应于枚举该类型值的不同方法。但是,为每个值指定应如何生成它可能是艰巨的。 Serial
类允许我们为生成特定类型的值指定一个很好的默认值。
Serial
的定义是
class Monad m => Serial m a where
series :: Series m a
所以它只是将特定Series m a
分配给m
和a
的给定组合。
coseries
有什么意义?需要生成功能类型的值。
Positive
中的SmallCheck.Series
类型?例如,像这样:
> 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.
当您编写Serial
实例(或任何Series
表达式)时,您在Series m
monad中工作。
在编写测试时,您将使用返回Bool
或Property m
的简单函数。
答案 2 :(得分:1)
虽然我认为@ tel的答案是一个很好的解释(我希望smallCheck
实际按照他描述的方式工作),但他提供的代码对我不起作用(使用smallCheck
版本1) 。我设法让以下工作......
更新/警告:下面的代码错误有一个相当微妙的原因。有关更正后的版本和详细信息,请参阅this answer下面提到的问题。简短版本是代替
instance Serial Identity Person
必须写instance (Monad m) => Series m Person
。
...但我发现使用Control.Monad.Identity
并且所有编译器标志都很奇怪,我已经问过separate question。
另请注意,虽然Series Person
(或实际上Series Identity Person
)实际上与函数Depth -> [Person]
不完全相同(请参阅@ tel的答案),但函数generate :: Depth -> [a] -> 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]))