了解 ScalaChecks' 'generation size'

Understanding ScalaChecks' 'generation size'

ScalaCheck 的 Gen API docs 解释 lazy val sized:

def sized[T](f: (Int) ⇒ Gen[T]): Gen[T]

Creates a generator that can access its generation size

看下面的例子:

import org.scalacheck.Gen.{sized, posNum}

scala> sized( s => { println(s); posNum[Int] })
res12: org.scalacheck.Gen[Int] = org.scalacheck.Gen$$anon@6a071cc0

scala> res12.sample
100
res13: Option[Int] = Some(12)

scala> res12.sample
100
res14: Option[Int] = Some(40)

generation size是什么意思,即上面输出的100?

生成大小是生成器将生成的结果数。 sized 方法只是让您编写知道自己大小的生成器,这样您就可以将该信息用作生成内容的一个因素。

例如,此生成器(来自 resource)生成两个数字列表,其中 1/3 为正数,2/3 为负数:

import org.scalacheck.Gen
import org.scalacheck.Prop.forAll

val myGen = Gen.sized { size =>
  val positiveNumbers = size / 3
  val negativeNumbers = size * 2 / 3
  for {
    posNumList <- Gen.listOfN(positiveNumbers, Gen.posNum[Int])
    negNumList <- Gen.listOfN(negativeNumbers, Gen.posNum[Int] map (n => -n))
  } yield (size, posNumList, negNumList)
}

check {
  forAll(myGen) {
    case (genSize, posN, negN) =>
     posN.length == genSize / 3 && negN.length == genSize * 2 / 3
  }
}

有点像 Scala 集合中的 zipWithIndexsized 只是为您提供元信息来帮助您做您需要做的事情。

sized 提供对 Scalacheck 的 "size" 参数的访问。此参数表示 "big" 生成器生成的值应该如何。此参数在几种情况下很有用:

  • 您想限制生成的值的数量以生成 属性 并因此更快地测试 运行。
  • 您需要将生成的数据纳入外部约束,例如检查字符串长度的表单验证器,或对列施加限制的数据库。
  • 您需要生成递归数据结构并在某个点终止。

Gen.sized 的同伴是 Gen.resize,它允许您更改生成器的大小,如 Gen.resize(10, Gen.alphaNumString) 中那样,它将生成不超过 10 个字符的字母数字字符串.

大多数内置生成器以某种方式使用 sized,例如 Gen.buildableOf(这是所有列表和容器生成器的基础):

[…] The size of the container is bounded by the size parameter used when generating values.

一个简单的例子

要了解如何使用 Gen.size,请查看 "Sized Generators"(Generators,Scalacheck 用户指南)中的示例:

def matrix[T](g: Gen[T]): Gen[Seq[Seq[T]]] = Gen.sized { size =>
  val side = scala.math.sqrt(size).asInstanceOf[Int]
  Gen.listOfN(side, Gen.listOfN(side, g))
}

此生成器使用 "size" 来限制矩阵的维度,以便整个矩阵的条目永远不会超过 "size" 参数。换句话说,如您的问题大小为 100,生成的矩阵将有 10 行和 10 列,总共有 100 个条目。

递归数据结构

"size" 对于确保递归数据结构的生成器终止特别有用。考虑以下生成二叉树实例并使用 size 限制每个分支的高度以确保生成器在某个点终止的示例:

import org.scalacheck.Gen
import org.scalacheck.Arbitrary.arbitrary

sealed abstract class Tree
case class Node(left: Tree, right: Tree, v: Int) extends Tree
case object Leaf extends Tree

val genLeaf = Gen.const(Leaf)
val genNode = for {
  v <- arbitrary[Int]
  left <- Gen.sized(h => Gen.resize(h/2, genTree))
  right <- Gen.sized(h => Gen.resize(h/2, genTree))
} yield Node(left, right, v)

def genTree: Gen[Tree] = Gen.sized { height =>
  if (height <= 0) {
    genLeaf
  } else {
    Gen.oneOf(genLeaf, genNode)
  }
}

请注意节点生成器如何递归生成树,但只允许生成 "size" 的一半。反过来,树生成器只会在其大小用尽时生成叶子。因此生成器的"size"是生成树高度的上限,确保生成器在某个点终止并且不会生成过大的树。

请注意,在此示例中,大小仅为树的高度设置了 上限 。它不影响生成树的平衡或生成具有一定深度的树的可能性。这些仅取决于 genTree 中定义的偏差。

oneOf 的情况下,每个子树有 50% 的几率只是一个叶子,在这个分支处结束树的生长,这使得生成一个完整的树,稍微耗尽了 "whole" 大小不太可能。

frequency(见下文)让您编码不同的偏差。在下面的示例中,节点比叶子更有可能,因此下面的生成器生成的树更有可能生长,但它仍然不太可能完整。

与Gen.frequency

的关系

Gen.frequency 用于不同的用例:您不会使用它来限制数据结构的深度或大小,而是为生成器的选择添加一定的偏差。看一下Gen.option的定义:

def option[T](g: Gen[T]): Gen[Option[T]] =
  frequency(1 -> const(None), 9 -> some(g))

此定义使用 frequency 使 None 的不太有趣的情况比 Some 更有趣的情况更不可能。

事实上,我们可以在上面的二叉树示例中组合 Gen.sizedGen.frequency,使 genTree 更有可能生成 "interesting" 个节点,而不是 [=88] =] 叶子:

def genTree: Gen[Tree] = Gen.sized { height =>
  if (height <= 0) {
    genLeaf
  } else {
    Gen.frequency(1 -> genLeaf, 9 -> genNode)
  }
}