Generating functions in Scalacheck
Scalacheck properties are a great way to test your code. In general, you use Scalacheck generators (Gen) to fabricate data that you use to test supposedly-invariant properties of your code.
Sometimes you even want to generate functions. Scalacheck also supports that. This article addresses how Scalacheck does that.
Gen monad
Gen is defined as a monad to allow composition of more complex generators from component generators. Its essence is something like this:
final case class Seed()
abstract case class Gen[A](run: Seed => (A, Seed)) {
def map[B](f: A => B): Gen[B]
def flatMap[B](f: A => Gen[B]): Gen[B]
}A Gen[A] generates A values.
Consuming values
To be able to implement a function, we need to be able to consume values as well as producing them. That is where Cogen[A] comes in.
A Cogen[A] consumes A values.
Cogen
Cogen’s essence is something like this:
Cogen is really just Gen with the arrows reversed, which is a pretty standard trick from category theory – reverse the arrows and stick “Co” in front of the name. Like with monad and comonad.
Also, Cogen is a contravariant functor, hence contramap. This means if you have a Cogen for any type A, you can also get a Cogen for any type S so long as you can convert a S into an A. That is contramap’s signature is trying to say.
Scalacheck provides some Cogen instances for us, defined in exactly this way:
object Cogen {
def apply[A](implicit F: Cogen[A]): Cogen[A] = F
}
implicit lazy val cogenLong: Cogen[Long] = ...
implicit lazy val cogenBoolean: Cogen[Boolean] =
Cogen[Long]
.contramap(b => if (b) 1L else 0L)
implicit lazy val cogenByte: Cogen[Byte] =
Cogen[Long]
.contramap(_.toLong)
implicit lazy val cogenShort: Cogen[Short] =
Cogen[Long]
.contramap(_.toLong)
// : you get the ideaGenerating a function
Armed with the means of generating and consuming values, we should be able to create a function. Consider this starting point:
def combine(seed0: Seed, cogen: Cogen[Long], gen: Gen[Boolean], n: Long): Boolean = {
val seed1 = cogen.perturb(n, seed0)
gen.run(seed1)._1
}It combines a Gen and a Cogen, converting a Long value n to a Boolean result. If we convert the n parameter to curried form, we get the equivalent code:
def combineCurried(seed0: Seed, cogen: Cogen[Long], gen: Gen[Boolean]): Long => Boolean =
n => {
val seed1 = cogen.perturb(n, seed0)
gen.run(seed1)._1
}But now this function returns a function – one from Long to Boolean.
Making this polymorphic:
def createFunction[A, B](seed0: Seed, cogen: Cogen[A], gen: Gen[B]): A => B =
n => {
val seed1 = cogen.perturb(n, seed0)
gen.run(seed1)._1
}Scalacheck internally uses a more baroque version of this mechanism to satisfy a generator for a function, ie Gen[A => B], when one is summoned like this: