Typelevel

and

ScalaCheck

Noel Markham (47 Degrees)

Agenda

  • The Gen class
  • Automatic arbitraries
  • Constraining types
  • Dates and times

Assumptions

  • Have worked with ScalaCheck before
  • Have worked with Cats or Scalaz before
    (or at least you know what a monad is)

Gen

Works great with Cats

Monad

val genMonad: Gen[String] = for {
  c <- Gen.alphaUpperChar
  s <- Gen.nonEmptyListOf(Gen.alphaLowerChar)
} yield (c :: s).mkString

Gen

Works great with Cats

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

Gen

Works great with Cats

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

Gen

Works great with Cats

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.

Gen

Works great with Cats

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!

Removing Arbitraries

Boiler plate removal, like a boss

We can use Shapeless's automatic typeclass derivation:

"com.github.alexarchambault"
             %% "scalacheck-shapeless_1.13" % "1.1.0-RC1"

Using scalacheck-shapeless

Boiler plate removal, like a boss

> 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._

Using scalacheck-shapeless

Boiler plate removal, like a boss

scala> arbitrary[Coordinates].sample
res0: Option[Coordinates] = Some(Coordinates(0,537278256,쪗ܘ暎))

scala> arbitrary[Coordinates].sample
res1: Option[Coordinates] = Some(Coordinates(-1204620568,0,掑὏꺒))

Using scalacheck-shapeless

This can derive any Arbitrary[A] as long as:

  • A is a case class or sealed trait family of case classes
  • An implicit Arbitrary can be summoned for A's constitent parts through automatic derivation or otherwise

But beware: compile times can dramatically increase!

Constraining types

What is the result of this property?

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.

Constraining types

Take II

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?

Constraining types

Introducing Refined

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!

Dates and Times

Introducing scalacheck-datetime

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])

Dates and Times

Introducing scalacheck-datetime

Features:

  • Works with both Joda time and Java 8's Date and Time API
  • Generates datetimes within a given range
  • Generates datetimes with a given granularity

See more at https://47deg.github.io/scalacheck-datetime