Skip to content

Commit 4cf7f8f

Browse files
authored
Merge pull request #359 from johnynek/oscar/tailrecm
Add Gen.product and Gen.tailRecM
2 parents c59ae70 + 1b14c68 commit 4cf7f8f

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

jvm/src/test/scala/org/scalacheck/GenSpecification.scala

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,37 @@ object GenSpecification extends Properties("Gen") {
325325

326326
property("some") = forAll { n: Int =>
327327
forAll(some(n)) {
328-
case Some(m) if m == n => true
328+
case Some(m) => m == n
329329
case _ => false
330330
}
331331
}
332332

333+
property("tailRecM") = forAll { (init: Int, seeds: List[Seed]) =>
334+
val g: ((Int, Int)) => Gen[Either[(Int, Int), Int]] =
335+
{
336+
case (c, x) if c <= 0 =>
337+
Gen.const(Right(x))
338+
case (c, x) =>
339+
val g = Gen.choose(Int.MinValue, x)
340+
g.map { i => Left(((c - 1), i)) }
341+
}
342+
343+
val g1 = Gen.tailRecM((10, init))(g)
344+
def g2(x: (Int, Int)): Gen[Int] = g(x).flatMap {
345+
case Left(y) => g2(y)
346+
case Right(x) => Gen.const(x)
347+
}
348+
349+
val finalG2 = g2((10, init))
350+
351+
352+
val params = Gen.Parameters.default
353+
354+
seeds.forall { seed =>
355+
g1.pureApply(params, seed) == finalG2.pureApply(params, seed)
356+
}
357+
}
358+
333359
property("uuid version 4") = forAll(uuid) { _.version == 4 }
334360

335361
property("uuid unique") = forAll(uuid, uuid) {

src/main/scala/org/scalacheck/Gen.scala

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,40 @@ object Gen extends GenArities{
467467
g.map(b.fromIterable)
468468
}
469469

470+
/** Monadic recursion on Gen
471+
* This is a stack-safe loop that is the same as:
472+
*
473+
* {{{
474+
*
475+
* fn(a).flatMap {
476+
* case Left(a) => tailRec(a)(fn)
477+
* case Right(b) => Gen.const(b)
478+
* }
479+
*
480+
* }}}
481+
*
482+
* which is useful for doing monadic loops without blowing up the
483+
* stack
484+
*/
485+
def tailRecM[A, B](a0: A)(fn: A => Gen[Either[A, B]]): Gen[B] = {
486+
@tailrec
487+
def tailRecMR(a: A, seed: Seed, labs: Set[String])(fn: (A, Seed) => R[Either[A, B]]): R[B] = {
488+
val re = fn(a, seed)
489+
val nextLabs = labs | re.labels
490+
re.retrieve match {
491+
case None => r(None, re.seed).copy(l = nextLabs)
492+
case Some(Right(b)) => r(Some(b), re.seed).copy(l = nextLabs)
493+
case Some(Left(a)) => tailRecMR(a, re.seed, nextLabs)(fn)
494+
}
495+
}
496+
497+
// This is the "Reader-style" appoach to making a stack-safe loop:
498+
// we put one outer closure around an explicitly tailrec loop
499+
gen[B] { (p: P, seed: Seed) =>
500+
tailRecMR(a0, seed, Set.empty) { (a, seed) => fn(a).doApply(p, seed) }
501+
}
502+
}
503+
470504
/** Wraps a generator lazily. The given parameter is only evaluated once,
471505
* and not until the wrapper generator is evaluated. */
472506
def lzy[T](g: => Gen[T]): Gen[T] = {

0 commit comments

Comments
 (0)