leanovate/gopter

Generating values which depend on each other fails

Opened this issue · 4 comments

I am looking for a way to generate values which depend on each other. In particular, I want to recreate the following simple example from ScalaCheck's User Guide (https://github.com/typelevel/scalacheck/blob/main/doc/UserGuide.md#generators), where two integer are generated, the first in the range of 10..20, the second has a lower bound which is twice as large as the first value:

// ScalaCheck Example
val myGen = for {
  n <- Gen.choose(10,20)
  m <- Gen.choose(2*n, 500)
} yield (n,m)

My impression was that the gen.FlatMap() should provide the required functionality (in Scala, <- is a monadic assignment, implemented by FlatMap), but I failed to find a way to succeed.

I defined a simple struct to generate two values which can be fed into the property:

type IntPair struct {
		Fst int
		Snd int
	}
properties.Property("ScalaCheck example for a pair", prop.ForAll(
		func(p IntPair) bool {
			a := p.Fst
			b := p.Snd
			return a*2 <= b
		},
		genIntPairScala(),
	))

The generator is a straight translation of the Scala code, first generating an integer and then generating a second via accessing the generated value of the first. Both generators are finally stored in the struct generator:

genIntPairScala := func() gopter.Gen {
		n := gen.IntRange(10, 20).WithLabel("n (fst)")
		m := n.FlatMap(func(v interface{}) gopter.Gen {
			k := v.(int)
			return gen.IntRange(2*k, 50)
		}, reflect.TypeOf(int(0))).WithLabel("m (snd)")

		var gen_map = map[string]gopter.Gen{"Fst": n, "Snd": m}
		return gen.Struct(
			reflect.TypeOf(IntPair{}),
			gen_map,
		)
	}

However, it does not work:

=== RUN   TestGopterGenerators
! ScalaCheck example for a pair: Falsified after 10 passed tests.
n (fst), m (snd): {Fst:17 Snd:32}
n (fst), m (snd)_ORIGINAL (1 shrinks): {Fst:19 Snd:32}
Elapsed time: 233.121µs
    properties.go:57: failed with initial seed: 1617636578517672000

Remark: I set the upper bound to 50 instead of 500. The property must still hold, but the generator has a smaller pool to pick suitable values: setting the upper bound to 500 often results in a passing property!

The problem here is that the final generator is not the result of a FlatMap. I.e. "n" and "m" are completely independent generators within the struct-generator

I thing the correct way would look something like this:

gen.IntRange(10, 20).FlatMap(func(v interface{}) gopter.Gen {
   n := v.(int)
   var gen_map = map[string]gopter.Gen{"Fst": gen.Const(n), "Snd": gen.IntRange(2*k, 50) }
   return gen.Struct(
			reflect.TypeOf(IntPair{}),
			gen_map,
		)
}

Hope this makes sense

Thanks, that works indeed. Here is the solution a bit reformatted:

genIntPair := func() gopter.Gen {
		return gen.IntRange(10, 20).FlatMap(func(v interface{}) gopter.Gen {
			k := v.(int)
			n := gen.Const(k)
			m := gen.IntRange(2*k, 50)
			var gen_map = map[string]gopter.Gen{"Fst": n, "Snd": m}
			return gen.Struct(
				reflect.TypeOf(IntPair{}),
				gen_map,
			)
		},
		reflect.TypeOf(int(0)))
	}

So the trick is that the first generated integer value must be re-introduced as generator by applying Const, the trivial generator (akin to return in a monadic setting).

If you have more dependencies some syntactic sugar would be nice, but this seems to be difficult in Go.

Scalacheck works well because of scala's for-comprehention notation, which is a very nice way to write these map/flatMap cascades.
Your example

val myGen = for {
  n <- Gen.choose(10,20)
  m <- Gen.choose(2*n, 500)
} yield (n,m)

actually expands to something like:

Gen.choose(10, 20).flatMap(n -> Gen.choose(2*n, 500).map(m -> (n,m))

I think you could write it like this in go as well, though you have to be very careful when using external variables in anonymous functions.

I like your FlatMap -> Map approach. This boils down to

genIntPair := func() gopter.Gen {
		return gen.IntRange(10, 20).FlatMap(func(v interface{}) gopter.Gen {
			k := v.(int)
			return gen.IntRange(2*k, 50).Map(func(m int) IntPair {
				return IntPair{Fst: k, Snd: m}
			})
		},
			reflect.TypeOf(int(0)))
	}

This is still baroque, but way more to the point than the first version. I will update my example PR #80