From 5336cf070b2a3f16778c13305ecbb0a802cde11a Mon Sep 17 00:00:00 2001 From: Oscar Boykin Date: Mon, 2 Oct 2017 20:02:06 -1000 Subject: [PATCH 1/3] Add Gen.product and Gen.tailRecM --- .../org/scalacheck/GenSpecification.scala | 22 ++++++++++ src/main/scala/org/scalacheck/Gen.scala | 40 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/jvm/src/test/scala/org/scalacheck/GenSpecification.scala b/jvm/src/test/scala/org/scalacheck/GenSpecification.scala index 39dc92e0c..b6c80c817 100644 --- a/jvm/src/test/scala/org/scalacheck/GenSpecification.scala +++ b/jvm/src/test/scala/org/scalacheck/GenSpecification.scala @@ -323,6 +323,12 @@ object GenSpecification extends Properties("Gen") { } } + property("product") = forAll { (a: Int, b: Int) => + forAll(Gen.product(a, b)) { + case (a1, b1) => a1 == a && b1 == b + } + } + property("some") = forAll { n: Int => forAll(some(n)) { case Some(m) if m == n => true @@ -330,6 +336,22 @@ object GenSpecification extends Properties("Gen") { } } + property("tailRecM") = forAll { (init: Int) => + val g: ((Int, Int)) => Gen[Either[(Int, Int), Int]] = + { + case (c, x) if c <= 0 => Right(x) + case (c, x) => Left((c - 1, x * Int.MaxValue)) + } + + val g1 = Gen.tailRecM((10, init))(g) + def g2(x: (Int, Int)): Gen[Int] = g(x).flatMap { + case Left(y) => g2(y) + case Right(x) => Gen.const(x) + } + + forAll(g1, g2((10, init)))(_ == _) + } + property("uuid version 4") = forAll(uuid) { _.version == 4 } property("uuid unique") = forAll(uuid, uuid) { diff --git a/src/main/scala/org/scalacheck/Gen.scala b/src/main/scala/org/scalacheck/Gen.scala index 2afc0990b..cdd874914 100644 --- a/src/main/scala/org/scalacheck/Gen.scala +++ b/src/main/scala/org/scalacheck/Gen.scala @@ -467,6 +467,46 @@ object Gen extends GenArities{ g.map(b.fromIterable) } + /** Monadic recursion on Gen + * This is a stack-safe loop that is the same as: + * + * {{{ + * + * fn(a).flatMap { + * case Left(a) => tailRec(a)(fn) + * case Right(b) => Gen.const(b) + * } + * + * }}} + * + * which is useful for doing monadic loops without blowing up the + * stack + */ + def tailRecM[A, B](a0: A)(fn: A => Gen[Either[A, B]]): Gen[B] = { + @tailrec + def tailRecMR[A, B](a: A, labs: Set[String])(fn: A => R[Either[A, B]]): R[B] = { + val re = fn(a) + val nextLabs = labs | re.labels + re.retrieve match { + case None => r(None, re.seed).copy(l = nextLabs) + case Some(Right(b)) => r(Some(b), re.seed).copy(l = nextLabs) + case Some(Left(a)) => tailRecMR(a, nextLabs)(fn) + } + } + + gen[B] { (p: P, seed: Seed) => + tailRecMR(a0, Set.empty) { a => fn(a).doApply(p, seed) } + } + } + + /** Build a pair from two generators + */ + def product[A, B](ga: Gen[A], gb: Gen[B]): Gen[(A, B)] = + for { + a <- ga + b <- gb + } yield (a, b) + /** Wraps a generator lazily. The given parameter is only evaluated once, * and not until the wrapper generator is evaluated. */ def lzy[T](g: => Gen[T]): Gen[T] = { From b817db8bd4987cb7cfb0f447a4f25c9a26dd1dd7 Mon Sep 17 00:00:00 2001 From: Oscar Boykin Date: Tue, 3 Oct 2017 09:44:36 -1000 Subject: [PATCH 2/3] improve tests and fix implementations --- .../org/scalacheck/GenSpecification.scala | 33 ++++++++++++++----- src/main/scala/org/scalacheck/Gen.scala | 23 ++++++++----- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/jvm/src/test/scala/org/scalacheck/GenSpecification.scala b/jvm/src/test/scala/org/scalacheck/GenSpecification.scala index b6c80c817..e46a37712 100644 --- a/jvm/src/test/scala/org/scalacheck/GenSpecification.scala +++ b/jvm/src/test/scala/org/scalacheck/GenSpecification.scala @@ -323,24 +323,34 @@ object GenSpecification extends Properties("Gen") { } } - property("product") = forAll { (a: Int, b: Int) => - forAll(Gen.product(a, b)) { - case (a1, b1) => a1 == a && b1 == b + property("product") = forAll { (a: Int, b: Int, seeds: List[Seed]) => + val ga = Gen.choose(Int.MinValue, a) + val gb = Gen.choose(Int.MinValue, b) + + val prod1 = Gen.product(ga, gb) + val prod2 = ga.flatMap { a => gb.map((a, _)) } + + val params = Gen.Parameters.default + seeds.forall { seed => + prod1.pureApply(params, seed) == prod2.pureApply(params, seed) } } property("some") = forAll { n: Int => forAll(some(n)) { - case Some(m) if m == n => true + case Some(m) => m == n case _ => false } } - property("tailRecM") = forAll { (init: Int) => + property("tailRecM") = forAll { (init: Int, seeds: List[Seed]) => val g: ((Int, Int)) => Gen[Either[(Int, Int), Int]] = { - case (c, x) if c <= 0 => Right(x) - case (c, x) => Left((c - 1, x * Int.MaxValue)) + case (c, x) if c <= 0 => + Gen.const(Right(x)) + case (c, x) => + val g = Gen.choose(Int.MinValue, x) + g.map { i => Left(((c - 1), i)) } } val g1 = Gen.tailRecM((10, init))(g) @@ -349,7 +359,14 @@ object GenSpecification extends Properties("Gen") { case Right(x) => Gen.const(x) } - forAll(g1, g2((10, init)))(_ == _) + val finalG2 = g2((10, init)) + + + val params = Gen.Parameters.default + + seeds.forall { seed => + g1.pureApply(params, seed) == finalG2.pureApply(params, seed) + } } property("uuid version 4") = forAll(uuid) { _.version == 4 } diff --git a/src/main/scala/org/scalacheck/Gen.scala b/src/main/scala/org/scalacheck/Gen.scala index cdd874914..fe715210b 100644 --- a/src/main/scala/org/scalacheck/Gen.scala +++ b/src/main/scala/org/scalacheck/Gen.scala @@ -484,28 +484,35 @@ object Gen extends GenArities{ */ def tailRecM[A, B](a0: A)(fn: A => Gen[Either[A, B]]): Gen[B] = { @tailrec - def tailRecMR[A, B](a: A, labs: Set[String])(fn: A => R[Either[A, B]]): R[B] = { - val re = fn(a) + def tailRecMR(a: A, seed: Seed, labs: Set[String])(fn: (A, Seed) => R[Either[A, B]]): R[B] = { + val re = fn(a, seed) val nextLabs = labs | re.labels re.retrieve match { case None => r(None, re.seed).copy(l = nextLabs) case Some(Right(b)) => r(Some(b), re.seed).copy(l = nextLabs) - case Some(Left(a)) => tailRecMR(a, nextLabs)(fn) + case Some(Left(a)) => tailRecMR(a, re.seed, nextLabs)(fn) } } + // This is the "Reader-style" appoach to making a stack-safe loop: + // we put one outer closure around an explicitly tailrec loop gen[B] { (p: P, seed: Seed) => - tailRecMR(a0, Set.empty) { a => fn(a).doApply(p, seed) } + tailRecMR(a0, seed, Set.empty) { (a, seed) => fn(a).doApply(p, seed) } } } /** Build a pair from two generators */ def product[A, B](ga: Gen[A], gb: Gen[B]): Gen[(A, B)] = - for { - a <- ga - b <- gb - } yield (a, b) + gen[(A, B)] { (p: P, seed: Seed) => + val ra = ga.doApply(p, seed) + ra.retrieve match { + case None => r(None, ra.seed).copy(l = ra.labels) + case Some(a) => + val rb = gb.doApply(p, ra.seed) + rb.map((a, _)).copy(l = ra.labels | rb.labels) + } + } /** Wraps a generator lazily. The given parameter is only evaluated once, * and not until the wrapper generator is evaluated. */ From 1b14c683189e7e45f27af74c68e99b4d008d9dc7 Mon Sep 17 00:00:00 2001 From: Oscar Boykin Date: Wed, 4 Oct 2017 11:56:30 -1000 Subject: [PATCH 3/3] remove product implementation --- .../scala/org/scalacheck/GenSpecification.scala | 13 ------------- src/main/scala/org/scalacheck/Gen.scala | 13 ------------- 2 files changed, 26 deletions(-) diff --git a/jvm/src/test/scala/org/scalacheck/GenSpecification.scala b/jvm/src/test/scala/org/scalacheck/GenSpecification.scala index e46a37712..8290bd9b3 100644 --- a/jvm/src/test/scala/org/scalacheck/GenSpecification.scala +++ b/jvm/src/test/scala/org/scalacheck/GenSpecification.scala @@ -323,19 +323,6 @@ object GenSpecification extends Properties("Gen") { } } - property("product") = forAll { (a: Int, b: Int, seeds: List[Seed]) => - val ga = Gen.choose(Int.MinValue, a) - val gb = Gen.choose(Int.MinValue, b) - - val prod1 = Gen.product(ga, gb) - val prod2 = ga.flatMap { a => gb.map((a, _)) } - - val params = Gen.Parameters.default - seeds.forall { seed => - prod1.pureApply(params, seed) == prod2.pureApply(params, seed) - } - } - property("some") = forAll { n: Int => forAll(some(n)) { case Some(m) => m == n diff --git a/src/main/scala/org/scalacheck/Gen.scala b/src/main/scala/org/scalacheck/Gen.scala index fe715210b..a6c59d365 100644 --- a/src/main/scala/org/scalacheck/Gen.scala +++ b/src/main/scala/org/scalacheck/Gen.scala @@ -501,19 +501,6 @@ object Gen extends GenArities{ } } - /** Build a pair from two generators - */ - def product[A, B](ga: Gen[A], gb: Gen[B]): Gen[(A, B)] = - gen[(A, B)] { (p: P, seed: Seed) => - val ra = ga.doApply(p, seed) - ra.retrieve match { - case None => r(None, ra.seed).copy(l = ra.labels) - case Some(a) => - val rb = gb.doApply(p, ra.seed) - rb.map((a, _)).copy(l = ra.labels | rb.labels) - } - } - /** Wraps a generator lazily. The given parameter is only evaluated once, * and not until the wrapper generator is evaluated. */ def lzy[T](g: => Gen[T]): Gen[T] = {