Typelevel
and
ScalaCheck
Noel Markham (47 Degrees)
Noel Markham (47 Degrees)
Assumptions
Monad
val genMonad: Gen[String] = for {
c <- Gen.alphaUpperChar
s <- Gen.nonEmptyListOf(Gen.alphaLowerChar)
} yield (c :: s).mkString
Monad
What about the laws?
property("Monad law: Left identity") = forAll { (arbInt: Int, add: Int) =>
arbInt < (Integer.MAX_VALUE - add) ==> {
val f: Int => Gen[Int] = i => Monad[Gen].pure(i + add)
val m1 = Monad[Gen].pure(arbInt).flatMap(f)
val m2 = f(arbInt)
m1.sample ?= m2.sample
}
}
... and the other laws hold too, honest
Applicative
val genApplicative: Gen[String] =
(Gen.alphaUpperChar |@| Gen.nonEmptyListOf(Gen.alphaLowerChar)).map {
(c, cs) =>
(c :: cs).mkString
}
If you don't need a monad, don't use one!
See the 2015 Scala World talk, Constraints liberate, liberties constrain
Monoid
val upperId = Gen.listOfN(3, Gen.alphaUpperChar)
val lowerId = Gen.listOfN(3, Gen.alphaLowerChar)
forAll(upperId |+| lowerId) { compositeId =>
// ...
}
As long as A
has a monoid, then Gen[A]
has a monoid too.
We're using the power of Cats to create more complicated generators from simple building blocks.
This is not ScalaCheck specific, this is simply functional programming!
We can use Shapeless's automatic typeclass derivation:
"com.github.alexarchambault"
%% "scalacheck-shapeless_1.13" % "1.1.0-RC1"
> test:console
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM).
Type in expressions to have them evaluated.
Type :help for more information.
scala> case class Coordinates(x: Int, y: Int, description: String)
defined class Coordinates
scala> import org.scalacheck.Shapeless._
import org.scalacheck.Shapeless._
scala> import org.scalacheck.Arbitrary._
import org.scalacheck.Arbitrary._
scala> arbitrary[Coordinates].sample
res0: Option[Coordinates] = Some(Coordinates(0,537278256,쪗ܘ暎))
scala> arbitrary[Coordinates].sample
res1: Option[Coordinates] = Some(Coordinates(-1204620568,0,掑꺒))
This can derive any Arbitrary[A]
as long as:
A
is a case class or sealed trait family of case classesArbitrary
can be summoned for A
's constitent parts through automatic derivation or otherwiseBut beware: compile times can dramatically increase!
property("Ordered integers") = forAll { (x: Int, y: Int, z: Int) =>
(x > 0 && y > 0 && z > 0) ==> {
val l = List(x, y, z).sorted
val min = l.head
val mid = l.drop(1).head
val max = l.last
(max - min >= 0) && max >= mid && mid >= min
}
}
[info] ! Constraints.Ordered integers:
Gave up after only 63 passed tests. 501 tests were discarded.
import Gen._
property("Ordered integers: II") = forAll(
posNum[Int], posNum[Int], posNum[Int]) {(x: Int, y: Int, z: Int) =>
val l = List(x, y, z).sorted
val min = l.head
val mid = l.drop(1).head
val max = l.last
(max - min >= 0) && max >= mid && mid >= min
}
[info] + Constraints.Ordered integers: II: OK, passed 100 tests.
Can we do better?
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
import eu.timepit.refined.scalacheck.numeric._
property("Ordered integers: III") = forAll {
(x: Int Refined Positive,
y: Int Refined Positive,
z: Int Refined Positive) =>
val l = List(x: Int, y: Int, z: Int).sorted
val min = l.head
val mid = l.drop(1).head
val max = l.last
(max - min >= 0) && max >= mid && mid >= min
}
Plenty more interesting refinements in the library!
A library for using sensible dates with ScalaCheck
scala> val now = ZonedDateTime.now
now: java.time.ZonedDateTime = 2016-09-01T10:31:40.801+01:00[Europe/London]
scala> val generator = genDateTimeWithinRange(now, Duration.ofDays(7))
generator: org.scalacheck.Gen[java.time.ZonedDateTime] =
org.scalacheck.Gen$$anon$5@34781a0f
scala> (1 to 3).foreach(_ => println(generator.sample))
Some(2016-09-06T06:48:42.275+01:00[Europe/London])
Some(2016-09-05T03:20:26.826+01:00[Europe/London])
Some(2016-09-06T06:06:35.392+01:00[Europe/London])
Features:
See more at https://47deg.github.io/scalacheck-datetime