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 idea
Generating 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: