我看到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
上,并更改leafs
和nodes
定义为:
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)
?
答案 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)