Scalacheck Произвольные импликации и рекурсивные генераторы
Я вижу, что кажется очень очевидной ошибкой в scalacheck, так что если это действительно так, я не вижу, как люди используют его для рекурсивных структур данных.
Эта программа выходит из строя с StackOverflowError
до того, как scalacheck возьмет верх, при построении значения Arbitrary
. Обратите внимание, что тип Tree
и генератор для Tree
принимается стенографически из этого учебника по скаляке.
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
s, и измените определение 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)
?
Ответы
Ответ 1
Проблема в том, что когда Scala оценивает trees
, он заканчивается бесконечной рекурсией, так как trees
определяется в терминах самого себя (через nodes
). Однако, когда вы помещаете какое-то другое выражение, чем trees
в качестве первой части вашего выражения для выражения в nodes
, Scala будет задерживать оценку остальной части выражения for (выражение в цепочках map
и flatMap
), и бесконечная рекурсия не произойдет.
Так же, как говорит педрофурла, если oneOf
был нестрогим, это, вероятно, не произойдет (поскольку Scala не будет немедленно оценивать аргументы). Однако вы можете использовать Gen.lzy
, чтобы быть явным о лени. lzy
принимает любой генератор и задерживает оценку этого генератора, пока он не будет использован. Таким образом, следующее изменение решает вашу проблему:
def trees: Gen[Tree] = Gen.lzy(Gen.oneOf(leafs, nodes))
Ответ 2
Несмотря на то, что после ответа Рикарда Нильсона выше, он избавился от константы 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
.
Ответ 3
Небольшое обобщение подхода в собственном ответе Даниэля Мартина использует 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)