gopter icon indicating copy to clipboard operation
gopter copied to clipboard

Generating values which depend on each other fails

Open alfert opened this issue 3 years ago • 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!

alfert avatar Apr 05 '21 15:04 alfert

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

untoldwind avatar Apr 13 '21 08:04 untoldwind

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.

alfert avatar Apr 15 '21 19:04 alfert

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.

untoldwind avatar Apr 17 '21 10:04 untoldwind

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

alfert avatar Apr 17 '21 17:04 alfert