如何使用ScalaCheck生成两个相同大小的集合

时间:2018-04-29 11:34:22

标签: scala scalacheck

我正在尝试为匹配算法开发基于属性的测试,我需要生成两个相同大小的输入集以提供给算法。我目前对解决方案的尝试如下:

case class Man(id: Long, quality: Long, ordering: Ordering[Woman])
case class Woman(id: Long, quality: Long, ordering: Ordering[Man])

val man: Gen[Man] = {
  for {
    id <- Gen.posNum[Long]
    quality <- Gen.posNum[Long]
  } yield Man(id, quality, Man.womanByQuality)
}

val woman: Gen[Woman] = {
  for {
    id <- Gen.posNum[Long]
    quality <- Gen.posNum[Long]
  } yield Woman(id, quality, Woman.manByQuality)
}  

def setOfN[T](n: Int, g: Gen[T]): Gen[Set[T]] = {
  Gen.containerOfN[Set, T](n, g)
}

def unMatched: Gen[(Set[Man], Set[Woman])] = Gen.sized {
  n => setOfN(n, man).flatMap(ms => setOfN(ms.size, woman).map(ws => (ms, ws)))
}

这会根据需要生成输入集的元组,但不保证它们的大小相同。当我使用...

运行测试时
property("all men and women are matched") = forAll(unMatched) {
  case (ms, ws) =>
    println((ms.size, ws.size))
    val matches = DeferredAcceptance.weaklyStableMatching(ms, ws)
    (matches.size == ms.size) && (matches.size == ws.size)
}

控制台将打印类似......

的内容
(0,0)
(1,1)
(2,2)
(3,2)
(1,2)
(0,2)
(0,1)
(0,0)
! marriage-market.all men and women are matched: Exception raised on proper
  ty evaluation.
> ARG_0: (Set(),Set(Woman(1,1,scala.math.Ordering$$anon$10@3d8314f0)))
> ARG_0_ORIGINAL: (Set(Man(3,1,scala.math.Ordering$$anon$10@2bea5ab4), Man(
  2,1,scala.math.Ordering$$anon$10@2bea5ab4), Man(2,3,scala.math.Ordering$$
  anon$10@2bea5ab4)),Set(Woman(1,1,scala.math.Ordering$$anon$10@3d8314f0), 
  Woman(3,2,scala.math.Ordering$$anon$10@3d8314f0)))
> Exception: java.lang.IllegalArgumentException: requirement failed
scala.Predef$.require(Predef.scala:264)
org.economicsl.matching.DeferredAcceptance$.weaklyStableMatching(DeferredAc
  ceptance.scala:97)
org.economicsl.matching.MarriageMarketSpecification$.$anonfun$new$2(Marriag
  eMarketSpecification.scala:54)
org.economicsl.matching.MarriageMarketSpecification$.$anonfun$new$2$adapted
  (MarriageMarketSpecification.scala:51)
org.scalacheck.Prop$.$anonfun$forAllShrink$2(Prop.scala:761)
Found 1 failing properties.

Process finished with exit code 1

测试失败,因为我已经要求两个输入集必须具有相同的大小。我的意图是生成器应该提供有效的输入数据。

思想?

3 个答案:

答案 0 :(得分:1)

问题:构造类型为Gen[(Set[T],Set[U])]的生成器,以便对于每个生成的集合对,该对中的每个集合都具有相同的大小。

以下功能

import org.scalacheck.Gen
def genSameSizeSets[T,U](gt: Gen[T], gu: Gen[U]): Gen[(Set[T],Set[U])] = {
  for { n      <- Gen.posNum[Long] // or .oneOf(1 to MAX_SET_SIZE)
        tset   <- Gen.containerOfN[Set,T](n, gt)
        uset   <- Gen.containerOfN[Set,U](n, gu)
        minsize = Math.min(tset.size, uset.size)
  } yield (tset.take(minsize), uset.take(minsize))
}

构造所需的生成器。

关于此生成器的一个关键点是,它完全避免了丢弃候选对象。

containerOfN本身不能保证所得Set的大小,因为这将需要gtgu来生成n连续的不同值。

另一种实现方式是将保护if子句放在 理解

if tset.size == uset.size

那可能是第一次尝试。它不是可靠的生成器,因为它具有很高的丢弃率,并且ScalaCheck在通过之前就放弃了。

在这种情况下,有一个简单的出路。与其丢弃不匹配的候选者,不如将其强制为与较小的候选者相同的大小(后者仍然是非空的)。由于设置值是任意的,因此丢弃哪个都没有关系。此逻辑是通过Math.mintake实现的。

这似乎是好的发电机设计的重要原则:“ 避免像瘟疫一样丢弃”。

这是一个完整的工作示例:

import org.scalacheck.Properties
import org.scalacheck.Gen
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.{forAll,collect}

object StackOverflowExample extends Properties("same size sets") {

  def genSameSizeSets[T,U](gt: Gen[T], gu: Gen[U]): Gen[(Set[T],Set[U])] = {
    for { n <- Gen.posNum[Int]
          ts <- Gen.containerOfN[Set,T](n, gt)
          us <- Gen.containerOfN[Set,U](n, gu)
          if us.size == ts.size
          minsize = Math.min(ts.size, us.size)
    } yield (ts.take(minsize), us.take(minsize))
  }

  val g = genSameSizeSets(Arbitrary.arbitrary[Int], Arbitrary.arbitrary[Char])

  property("same size")  = forAll(g) { case (intSet, charSet) =>
    collect(intSet.size, charSet.size) { intSet.size == charSet.size }
  }


}

具有此输出

+ same size sets.same size: OK, passed 100 tests.
> Collected test data: 
8% (11,11)
7% (2,2)
7% (17,17)
6% (16,16)
<snip>
1% (44,44)
1% (27,27)
1% (26,26)
1% (56,56)

答案 1 :(得分:0)

我偶然发现了以下解决方案。

def unMatched: Gen[(Set[Man], Set[Woman])] = Gen.sized {
  n => setOfN(n, man).flatMap(ms => setOfN(ms.size, woman).map(ws => (ms, ws))).suchThat { case (ms, ws) => ms.size == ws.size }
}

但我认为不应该使用suchThat组合子。问题似乎是size参数被视为容器大小的上限(而不是等式约束)。

根据@FlorianK的评论更新

我发现问题出在我对ManWoman生成器的规范上。这些生成器不是生成器不同的值。而不是使用肯定的Long来表示我使用Java id切换到的唯一UUID。正确的发电机是

val man: Gen[Man] = {
  for {
    id <- Gen.uuid
    quality <- Gen.posNum[Long]
  } yield Man(id, quality, Man.womanByQuality)
}

val woman: Gen[Woman] = {
  for {
    id <- Gen.uuid
    quality <- Gen.posNum[Long]
  } yield Woman(id, quality, Woman.manByQuality)
}

我不太确定为什么原始发电机没有按预期工作。他们当然有可能产生非独特的实例,但我认为它应该非常罕见(猜我错了!)。

答案 2 :(得分:0)

我设法使用以下代码生成大小相等的List[Int]对:

val pairOfListGen = Gen.sized { size => for {
    x <- Gen.containerOfN[List, Int](size, Gen.choose(0,50000))
    y <- Gen.containerOfN[List, Int](size, Gen.choose(0,50000))
  } yield (x,y)
}

Man.womanByQuality未在您的代码示例中定义,因此我无法使用您的生成器对其进行测试,但我希望这对您有用。