Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/introduce continuation arbs builder #2494

Merged
merged 10 commits into from
Sep 18, 2021

Conversation

myuwono
Copy link
Contributor

@myuwono myuwono commented Sep 11, 2021

This is a proposed implementation for arbitrary builders using continuation passing style. This code is inspired by how monad fx on any Kind<F, A> was previously done in arrow.

This allows the following simplification as described in #2493:

data class Foo(val first: String, val multiplicationResult: Long)

val arbFirst: Arb<String> = ...
fun arbSecond(first: String): Arb<Int> = ...
val arbThird: Arb<Long> = ...

// with raw binds
val arbBind: Arb<String> = arbFirst.flatMap { first ->
   Arb.bind(arbSecond, arbThird) { second, third -> 
      Foo(first, second.toLong() * third)
   }
} 
// with continuation
// notice we also have access to the random source
val arb: Arb<String> = arbitrary { rs: RandomSource ->
   val first = arbFirst.bind()
   val multiplicationResult = arbSecond(first).bind().toLong() * arbThird.bind()
   Foo(first, multiplicationResult)
}

fixes #2493

@sksamuel
Copy link
Member

sksamuel commented Sep 11, 2021

You could also do this I think ?

val arb: Arb<String> = arbitrary { rs ->
   val first = Arb.string(5, Codepoint.alphanumeric()).withEdgecases("edge1", "edge2").next(rs)
   val second = Arb.int(1..9).withEdgecases(5).next(rs)
   val third = Arb.int(101..109).withEdgecases(100 + second, 109).next(rs)
   "$first $second $third"
}

@myuwono
Copy link
Contributor Author

myuwono commented Sep 11, 2021

You could also do this I think ?

val arb: Arb<String> = arbitrary { rs ->
   val first = Arb.string(5, Codepoint.alphanumeric()).withEdgecases("edge1", "edge2").next(rs)
   val second = Arb.int(1..9).withEdgecases(5).next(rs)
   val third = Arb.int(101..109).withEdgecases(100 + second, 109).next(rs)
   "$first $second $third"
}

You could, the downside is one would have to be prepared to also forego the edgecases of the dependent arbs.

I guess, the general question is, how might we chain arbs such that all the intrinsics are propagated fully? We look at flatmap for instance as a central point of coordination. That solves edgecases compositions without cross-contaminating samples, and potentially many other possibilities such as propagation of shrinkers and classifications.

@sksamuel
Copy link
Member

sksamuel commented Sep 11, 2021 via email

@sksamuel
Copy link
Member

Can you talk me through the implementation a bit. At least I'm not clear how the returnedArb gets populated in the continuation exactly.

@myuwono
Copy link
Contributor Author

myuwono commented Sep 12, 2021

Can you talk me through the implementation a bit. At least I'm not clear how the returnedArb gets populated in the continuation exactly.

awesome. so i think i'd properly quote my reference in https://github.com/arrow-kt/arrow-core/blob/40619f87b7fc1c790c3ab7b7341005a960630a27/arrow-core-data/src/main/kotlin/arrow/typeclasses/Monad.kt

This looks a bit weird at first, but it is actually quite simple. It's in fact very clever!

so let's try this code for instance:

arbitrary {
  val string: String = Arb.string(5).bind()
  val int: Int = Arb.int(1..100).bind()
  "$string $int"
}

now the sequence actually happen as follows

1. suspending computation: at line 2, i.e. when bind() is called.

that executes this function

   override suspend fun <B> Arb<B>.bind(): B = suspendCoroutineUninterceptedOrReturn { c ->
      // we call flatMap on the bound arb, and then returning the `returnedArb`, without modification
      returnedArb = this.flatMap { b: B ->
         // we resume the suspension with the value passed inside the flatMap function. 
         // this can be either sample or edgecases. This is important
         // because from the point of view of a user of kotest, when we talk about transformation, 
         // we  care about transforming the generated value of this arb for both sample and edgecases.
         c.resume(b) 
         returnedArb // returned arb here is returned as is
      }
      COROUTINE_SUSPENDED
   }

Notice this block returns the special COROUTINE_SUSPENDED value, this means the Continuation provided to the block shall be resumed by invoking Continuation.resumeWith at some moment in the future when the result becomes available to resume the computation. That resume with is as follows:

   override fun resumeWith(result: Result<Arb<A>>) {
     result.map { theNewArb -> returnedArb = theNewArb }.getOrThrow()
   }

if you realize, this returnedArb is only going to be modified at the end of the suspend computation, which we defined at the call site where we build the final Arb:

   // continuation is a single shot so this needs to be a function to reinstate value randomization through the arbs
   private fun computeArb(): Arb<A> {
      val continuation = ArbContinuation<A>()
      
      // this is where the point action happen, i.e. when we build the final arb from the returned value from the block.
      // essentially at the end of the suspend we got `A`, and here we need to point it into `Arb<A>`
      val wrapReturn: suspend ArbitraryBuilderSyntax.() -> Arb<A> = {
         // the value after finishing our builder block
         val value: A = builderFn(randomSource.bind())
         // we then create a new arb here with that value.
         ArbitraryBuilder(
            { value },
            classifier,
            shrinker,
            { rs -> edgecaseFn?.invoke(rs) ?: value }
         ).build()
      }
 
      // all the functions are set, we start our coroutine, providing our continuation instance, this will run the suspend block
      wrapReturn.startCoroutine(continuation, continuation)
      
      // once that is finished, the `continuation.resumeWith` will be called, where we assign the `returnedArb` var. 
      // after that, we then return that populated value.
      return continuation.returnedArb()
   }

@myuwono
Copy link
Contributor Author

myuwono commented Sep 12, 2021

I'll pop those comments in the code as well

@sksamuel
Copy link
Member

Can we add the generateArbitrary variant which allows suspension in the lamba (inline?)
Could be another pr.

@myuwono
Copy link
Contributor Author

myuwono commented Sep 18, 2021

added inline suspend fun <A> generateArbitrary(...): Arb<A> 🎉

@sksamuel sksamuel merged commit bcae7d7 into master Sep 18, 2021
@sksamuel sksamuel deleted the feature/introduce-continuation-arbs-builder branch September 18, 2021 01:44
@myuwono myuwono mentioned this pull request Oct 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[proposal] introduce continuation passing style arbitrary builder for improved ergonomics
2 participants