mitch
02/10/2021, 8:02 PMsam
02/10/2021, 8:03 PMmitch
02/10/2021, 8:12 PMsam
02/10/2021, 8:14 PMmitch
02/10/2021, 8:22 PMsam
02/10/2021, 8:24 PMmitch
02/10/2021, 8:25 PMEdgeCases.Random
as a sealed class member may raise some eyebrows (mine included). If i understand your comment correctly, is this what you’re thinking?
sealed class EdgeCases<out A> {
data class Static<A>(val values: NonEmptyList<A>) : EdgeCases<A>()
data class SemiRandom<A>(val generate: (RandomSource) -> NonEmptyList<A>) : EdgeCases<A>()
}
When we’re operating in Arb
composition, the challenge is random seed would be provided much later. That means we need to think of a way to model EdgeCases
with the same principle to harvest rs
.
In other words, if I were to oversimplify this a bit, we’ll probably get
data class EdgeCases<out A>(val generate: (RandomSource) -> NonEmptyList<A>)
sam
02/13/2021, 11:07 PMfun edgecases(): ...
to fun edgecases(rs: RandomSource):...
and then it's up to the arb if they want to return a static list or notfun edges()
fun semiedges()
kind of thing (not sure if that's too much)fun edges() : (RandomSource) -> List<A>
which defaults to just = { edgecases() }
mitch
02/13/2021, 11:12 PMSo one thing would be to changetofun edgecases(): ...
and then it’s up to the arb if they want to return a static list or not,fun edgecases(rs: RandomSource):...
another way would be to return a function that accepts the RS and then returns, sowhich defaults to justfun edges() : (RandomSource) -> List<A>
= { edgecases() }
I think the easiest way would be to pass the random source into the edgecases method, and then Arb.bind can use that RS to call .sample on whatever it needsyeah i do agree, that’s actually converging to this:
data class EdgeCases<out A>(val generate: (RandomSource) -> NonEmptyList<A>)
except that we push the responsibility of conjuring edgecases to the arb implementation. That will work from the types perspective, implementation details can follow.sam
02/13/2021, 11:12 PMmitch
02/13/2021, 11:13 PMArb<A> {
fun edges(rs: RandomSource): NonEmptyList<A>
}
We can also use List<A> with a check
that edgecases needs to be nonempty at generation time.. But i prefer that check done at compile timesam
02/13/2021, 11:18 PMdata class Person(val name: String)
mitch
02/13/2021, 11:21 PMArb<A>
implementers can override edges to emptyList()
but yeah that’s legal as well. given we can guard it in flatMap that’s okay.sam
02/13/2021, 11:22 PMfun edgecases(rs: RandomSource)
so it matches the other one ?mitch
02/13/2021, 11:22 PMsam
02/13/2021, 11:22 PMmitch
02/13/2021, 11:28 PMArb<A> {
fun edgecases(rs: RandomSource): List<A>
}
bind will
bind(arbA, arbB): Arb<B> {
fun edgecases(rs: RandomSource): List<A> = // find out if any of the A or B edgecases is empty and default accordingly
}
flatmap will also work
Arb<A>.flatMap(fn: (A) -> Arb<B>): Arb<B> {
fun edgecases(rs: RandomSource): List<B> = this@flatMap.edgecases(rs).flatMap { a -> fn(a) // resolve edges here }
}
sam
02/13/2021, 11:28 PMmitch
02/13/2021, 11:28 PMthe bind impl might need to change too, currently bindN delegates to bindN-1what do you mean here?
sam
02/13/2021, 11:28 PMgenA.bind(genB).bind(genC).bind(genD).bind(genE).bind(genF).bind(genG).map { (abcdef, g) ->
val (abcde, f) = abcdef
val (abcd, e) = abcde
val (abc, d) = abcd
val (ab, c) = abc
val (a, b) = ab
bindFn(a, b, c, d, e, f, g)
mitch
02/13/2021, 11:30 PMApplicative[F[_]] {
product(a: F[A], b: F[B]): F[(A, B)]
}
and then the rest will work by recursively applying productsam
02/13/2021, 11:31 PMmitch
02/14/2021, 12:52 AMArb<A>.withEdgecases(fn: (RandomSeed) -> List<A>): Arb<A>
and
Arb<A>.modifyEdgecases(fn: (initial: List<A>, rs: RandomSeed) -> List<A>): Arb<A>
sam
02/14/2021, 12:53 AMmitch
02/14/2021, 1:21 AMOverload resolution ambiguity. All these functions match.
public fun <A> Arb<TypeVariable(A)>.modifyEdgecases(f: (initial: List<TypeVariable(A)>, randomSource: RandomSource) → List<TypeVariable(A)>): Arb<TypeVariable(A)> defined in io.kotest.property.arbitrary in file arbs.kt
public fun <A> Arb<TypeVariable(A)>.modifyEdgecases(f: (List<TypeVariable(A)>) → List<TypeVariable(A)>): Arb<TypeVariable(A)> defined in io.kotest.property.arbitrary in file arbs.kt
kotlin can’t resolve pick up lambda types, I think i need to hack this to use RandomSource as receiver type if we were to maintain backward compatibilty
fun <A> Arb<A>.modifyEdgecases(f: RandomSource.(List<A>) -> List<A>): Arb<A> =
arbitrary({ rs -> f(rs, this@modifyEdgecases.edgecases(rs)) }, { this.next(it) })
sam
02/14/2021, 1:27 AMmitch
02/14/2021, 1:28 AMfun <A> Arb<A>.modifyEdgecases(f: (List<A>) -> List<A>): Arb<A> = TODO()
fun <A> Arb<A>.modifyEdgecases(f: (List<A>, RandomSource) -> List<A>): Arb<A> = TODO()
sam
02/14/2021, 1:28 AMfun <A> Arb<A>.modifyEdgecases(f: (List<A>) -> List<A>): Arb<A> = TODO()
@JvmName("modifyEdgecasesRandom")
fun <A> Arb<A>.modifyEdgecases(f: (List<A>, RandomSource) -> List<A>): Arb<A> = TODO()
mitch
02/14/2021, 1:29 AMsam
02/14/2021, 1:31 AMlistOf(2,3) + it
mitch
02/14/2021, 1:32 AMnope kotlin gave up here toolistOf(2,3) + it
sam
02/14/2021, 1:33 AMmitch
02/14/2021, 1:33 AMsam
02/14/2021, 1:33 AMmitch
02/14/2021, 1:34 AMsam
02/14/2021, 1:35 AMa -> a + listOf(2,3)
mitch
02/14/2021, 1:35 AMsam
02/14/2021, 1:35 AMmitch
02/14/2021, 1:36 AMsam
02/14/2021, 1:36 AMmitch
02/14/2021, 2:36 AMArb<A> {
fun edgecases(rs: RandomSource): List<A>
}
sam
02/14/2021, 2:36 AMmitch
02/14/2021, 2:37 AM"Arb.bind(a,b,c,d) should compute the cartesian product of edges" {
val arbA = Arb.string(1)
val arbB = Arb.string().withEdgecases("a", "b")
val arbC = Arb.string(1)
val arbD = Arb.string().withEdgecases("a", "b")
val arb = Arb.bind(arbA, arbB, arbC, arbD) { a, b, c, d -> a + b + c + d }
arb.edgecases(RandomSource.seeded(1234L)) shouldContainExactlyInAnyOrder listOf(
"Uaka", "%bca", "?aCb", "KbGb"
)
}
instead of generating
["Uaka", "%bca", "?aCb", "KbGb"]
it generates
["Uaka", "Uakb", "%bca", "%bcb"]
sam
02/14/2021, 2:38 AMmitch
02/14/2021, 2:39 AMprivate fun <A, B, C> Gen<A>.bind(other: Gen<B>, fn: (A, B) -> C): Arb<C> {
val thisArb = when (this) {
is Arb -> this
is Exhaustive -> this.toArb()
}
val otherArb = when (other) {
is Arb -> other
is Exhaustive -> other.toArb()
}
val arb = thisArb.flatMap { a -> otherArb.map { b -> fn(a, b) } }
return object : Arb<C>() {
override fun edgecases(): List<C> = arb.edgecases()
override fun edgecases(rs: RandomSource): List<C> {
val maybeEdgesA = NonEmptyList.fromList(thisArb.edgecases(rs))
val maybeEdgesB = NonEmptyList.fromList(otherArb.edgecases(rs))
return maybeEdgesA.fold(
ifEmpty = {
maybeEdgesB.fold(
ifEmpty = {
fn(thisArb.single(rs), otherArb.single(rs)).nel()
},
ifDefined = { edgesB ->
edgesB.map { b ->
val a = thisArb.single(rs)
fn(a, b)
}
}
)
},
ifDefined = { edgesA ->
maybeEdgesB.fold(
ifEmpty = {
edgesA.map { a: A ->
val b = otherArb.single(rs)
fn(a, b)
}
},
ifDefined = { edgesB ->
edgesA.flatMap { a: A ->
edgesB.map { b: B ->
fn(a, b)
}
}
}
)
}
).all
}
override fun sample(rs: RandomSource): Sample<C> = arb.sample(rs)
override fun values(rs: RandomSource): Sequence<Sample<C>> = arb.values(rs)
}
}
sam
02/14/2021, 2:40 AMmitch
02/14/2021, 2:41 AMArb<A> {
fun edgecases(): (rs: RandomSource) -> List<A>
}
sam
02/14/2021, 2:41 AMmitch
02/14/2021, 2:41 AM(rs: RandomSource) -> List<A>
is a kleislisam
02/14/2021, 2:48 AMfun <A> edgesOrSample(edges: List<A>, gen: Gen<A>, rs: RandomSource): List<A> =
if (edges.isEmpty()) listOf(gen.generate(rs).first().value) else edges
fun <A, B, C> Gen<A>.bind(other: Gen<B>, fn: (A, B) -> C): Arb<C> {
val thisArb = when (this) {
is Arb -> this
is Exhaustive -> this.toArb()
}
val otherArb = when (other) {
is Arb -> other
is Exhaustive -> other.toArb()
}
return object : Arb<C>() {
override fun edgecases(): List<C> = TODO() // previous cartesian combination
override fun edgecases(rs: RandomSource): List<C> {
val maybeEdgesA = thisArb.edgecases(rs)
val maybeEdgesB = otherArb.edgecases(rs)
// we don't create edges is none of the bound gens have edge cases
return if (maybeEdgesA.isEmpty() && maybeEdgesB.isEmpty())
emptyList()
else
edgesOrSample(maybeEdgesA, this@bind, rs).flatMap { a ->
edgesOrSample(maybeEdgesB, other, rs).map { b ->
fn(a, b)
}
}
}
override fun sample(rs: RandomSource): Sample<C> = arb.sample(rs)
}
}
mitch
02/14/2021, 2:55 AMedgesOrSample
returns a concrete listsam
02/14/2021, 2:55 AMsam
02/14/2021, 2:56 AMmitch
02/14/2021, 2:57 AMsam
02/14/2021, 3:00 AMmitch
02/14/2021, 3:05 AMr1 a r2 a
r3 a r4 b
r5 b r6 a
r7 b r8 b
Instead we get
r1 a r2 a
r1 a r2 b
r3 b r4 a
r3 b r4 b
notice how r1 and r2 as well as r3 and r4 are repeated because of concrete listsam
02/14/2021, 3:05 AMmitch
02/14/2021, 3:05 AMsam
02/14/2021, 3:06 AMmitch
02/14/2021, 3:07 AMList<Edge<A>>
List<A>
sam
02/14/2021, 3:07 AMmitch
02/14/2021, 3:07 AMEdge<A> -> A
on every iterationval list = edgesOrSample(maybeEdgesA, this@bind, rs) // e.g. a, b
val anotherList = edgesOrSample(maybeEdgesB, this@bind, rs) // e.g. r1, r2
list.flatMap { first -> anotherList.map { second -> first to second } }
// a -> r1
// a -> r2
// b -> r1
// b -> r2
sam
02/14/2021, 3:11 AMedgesOrSample(maybeEdgesA, this@bind, rs).flatMap { a ->
edgesOrSample(maybeEdgesB, other, rs).map { b ->
fn(a, b)
}
}
mitch
02/14/2021, 3:12 AMsam
02/14/2021, 3:12 AMmitch
02/14/2021, 3:13 AMsam
02/14/2021, 3:13 AMmitch
02/14/2021, 3:13 AMsam
02/14/2021, 3:14 AMmitch
02/14/2021, 3:16 AMa, *
should work for any *1, x === 1, 1 or 1, 0
sam
02/14/2021, 3:17 AMval arbA = Arb.string().withEdgecases("a", "b")
val arbB = Arb.string().withEdgecases("c", "d")
val arbC = Arb.string().withEdgecases("e", "f")
val arbD = Arb.string(1)
In that scenario, I would probably expect (a, c, e, Z), (b, c, e, Z), (a, d, e, Z), (b, d, e, Z) and so on. Z being a random, but fixed element.mitch
02/14/2021, 3:26 AMIn that scenario, I would probably expect (a, c, e, Z), (b, c, e, Z), (a, d, e, Z), (b, d, e, Z) and so on. Z being a random, but fixed element.yeah ok so using the code sample you gave me i got the above behaviour, because of
list.flatMap
sam
02/14/2021, 3:27 AMmitch
02/14/2021, 3:32 AMfun list() = edgesOrSample(maybeEdgesA, this@bind, rs) // e.g. r1, r2
fun anotherList() = edgesOrSample(maybeEdgesB, this@bind, rs) // e.g. a, b
list.flatMap { first -> anotherList.map { second -> first to second } }
// r1 -> a
// r1 -> b
// r2 -> a
// r2 -> b
so this is the behaviour that you are after with
(a, c, e, Z), (b, c, e, Z), (a, d, e, Z), (b, d, e, Z)
sam
02/14/2021, 3:33 AMmitch
02/14/2021, 3:36 AMsam
02/14/2021, 3:37 AMmitch
02/15/2021, 12:30 PMEval
type for trampolining etc. and I’m still in the middle of implementing flatmap and doing all the type gymnastics. it wasn’t worth it.sam
02/15/2021, 12:33 PMmitch
02/15/2021, 12:34 PM(a, c, e, Z), (b, c, e, Z), (a, d, e, Z), (b, d, e, Z)
sam
02/15/2021, 12:35 PMmitch
02/15/2021, 12:36 PMsam
02/15/2021, 12:37 PMmitch
02/15/2021, 12:39 PM