scalacheck任意隐含和递归生成器

时间:2013-11-07 06:22:47

标签: scala recursive-datastructures scalacheck

我看到scalacheck的一个非常明显的错误,如果真的存在,我就看不出人们如何将它用于递归数据结构。

在构建StackOverflowError值时,此程序在scalacheck接管之前失败并显示Arbitrary。请注意,Tree类型和Tree的生成器是从this scalacheck tutorial逐字记录的。

package treegen

import org.scalacheck._
import Prop._

class TreeProperties extends Properties("Tree") {

  trait Tree
  case class Node(left: Tree, right: Tree) extends Tree
  case class Leaf(x: Int) extends Tree

  val ints = Gen.choose(-100, 100)

  def leafs: Gen[Leaf] = for {
    x <- ints
  } yield Leaf(x)

  def nodes: Gen[Node] = for {
    left <- trees
    right <- trees
  } yield Node(left, right)

  def trees: Gen[Tree] = Gen.oneOf(leafs, nodes)

  implicit lazy val arbTree: Arbitrary[Tree] = Arbitrary(trees)

  property("vacuous") = forAll { t: Tree => true }
}

object Main extends App {
  (new TreeProperties).check
}

更奇怪的是,不应该影响任何事情的变化似乎会改变程序,使其有效。例如,如果您将trees的定义更改为此值,则会毫无问题地通过:

  def trees: Gen[Tree] = for {
    x <- Gen.oneOf(0, 1)
    t <- if (x == 0) {leafs} else {nodes}
  } yield t

更奇怪的是,如果您更改二叉树结构以使值存储在Node而不是Leaf上,并更改leafsnodes定义为:

  def leafs: Gen[Leaf] = Gen.value(Leaf())

  def nodes: Gen[Node] = for {
    x <- ints     // Note: be sure to ask for x first, or it'll StackOverflow later, inside scalacheck code!
    left <- trees
    right <- trees
  } yield Node(left, right, x)

它也可以正常工作。

这里发生了什么?为什么最初构造Arbitrary值导致堆栈溢出?为什么scalacheck生成器似乎对不应影响生成器控制流的微小变化如此敏感?

为什么我的上述表达式与oneOf(0, 1)完全等同于原始oneOf(leafs, nodes)

3 个答案:

答案 0 :(得分:12)

即使按照Rickard Nilsson上面的回答摆脱了程序启动时的常量StackOverflowError,我实际问过的时候,我仍然会在三次中遇到StackOverflowError一次scalacheck检查属性。 (我将Main更改为运行.check 40次,并且会看到它成功两次,然后堆栈溢出失败,然后成功两次等等。)

最终我不得不在递归的深度上加入一个硬块,这就是我想在将来对递归数据结构使用scalacheck时我会做的事情:

  def leafs: Gen[Leaf] = for {
    x <- ints
  } yield Leaf(x)

  def genNode(level: Int): Gen[Node] = for {
    left <- genTree(level)
    right <- genTree(level)
  } yield Node(left, right)

  def genTree(level: Int): Gen[Tree] = if (level >= 100) {leafs}
                                       else {leafs | genNode(level + 1)}
  lazy val trees: Gen[Tree] = genTree(0)

通过此更改,scalacheck永远不会遇到StackOverflowError

答案 1 :(得分:11)

问题在于,当Scala评估trees时,它会以无限递归结束,因为trees是根据自身定义的(通过nodes)。但是,当您在trees中添加除nodes之外的其他表达式作为for表达式的第一部分时,Scala将延迟对for表达式的其余部分的求值(包含在{的链中) {1}}和map调用),无限递归不会发生。

正如pedrofurla所说,如果flatMap不严格,这可能不会发生(因为Scala不会立即评估参数)。但是,您可以使用oneOf来明确懒惰。 Gen.lzy接受任何生成器并延迟对该生成器的评估,直到它真正被使用。因此,以下更改可以解决您的问题:

lzy

答案 2 :(得分:2)

丹尼尔·马丁自己的答案中略微概括的方法是使用sized。像(未经测试)的东西:

def genTree() = Gen.sized { size => genTree0(size) }

def genTree0(maxDepth: Int) = 
  if (maxDepth == 0) leafs else Gen.oneOf(leafs, genNode(maxDepth))

def genNode(maxDepth: Int) = for {
  depthL <- Gen.choose(0, maxDepth - 1)
  depthR <- Gen.choose(0, maxDepth - 1)
  left <- genTree0(depthL)
  right <- genTree0(depthR)
} yield Node(left, right)

def leafs = for {
  x <- ints
} yield Leaf(x)