Skip to content

ScalaCheck Derivation

Derives Arbitrary, Cogen, and Shrink instances for case classes, sealed traits, Scala 3 enums, Java enums, and more. Replaces manual instance definitions and scalacheck-shapeless.

Installation

sbt

libraryDependencies += "com.kubuszok" %% "kindlings-scalacheck-derivation" % "0.2.0"

Cross-platform (JVM / Scala.js / Scala Native):

libraryDependencies += "com.kubuszok" %%% "kindlings-scalacheck-derivation" % "0.2.0"

Scala CLI

//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0

Note

You also need scalacheck as a runtime dependency:

libraryDependencies += "org.scalacheck" %%% "scalacheck" % "1.19.0"

Quick start

Generating random case class instances
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0

import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.Arbitrary

case class Person(name: String, age: Int)

// Extension method on Arbitrary companion
implicit val arbPerson: Arbitrary[Person] = Arbitrary.derived[Person]

// Generate a random sample — derivation produces valid instances
val sample = Arbitrary.arbitrary[Person].sample
println(sample.isDefined)
// expected output:
// true

API

Derivation methods

ScalaCheck derivation works via extension methods on the ScalaCheck companion objects and via standalone Derive* objects:

Method Returns Description
Arbitrary.derived[A] Arbitrary[A] Derive via extension method on companion
DeriveArbitrary.derived[A] Arbitrary[A] Derive via standalone object
Cogen.derived[A] Cogen[A] Derive via extension method on companion
DeriveCogen.derived[A] Cogen[A] Derive via standalone object
Shrink.derived[A] Shrink[A] Derive via extension method on companion
DeriveShrink.derived[A] Shrink[A] Derive via standalone object

No configuration class is needed -- derivation is fully automatic based on the type structure.

Supported types

  • Case classes -- generates random values for each field
  • Sealed traits / enums -- randomly selects a subtype
  • Scala 3 enums -- randomly selects an enum value
  • Java enums -- randomly selects an enum constant
  • Recursive types -- handled automatically, no Lazy wrappers needed
  • Named tuples -- supported on Scala 3
  • Opaque types -- supported when the underlying type has an instance
  • Collections and Option -- standard library instances are used

Usage examples

Sealed trait with property-based testing
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0

import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.{Arbitrary, Prop}

sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape

implicit val arbShape: Arbitrary[Shape] = Arbitrary.derived[Shape]

val prop = Prop.forAll { (shape: Shape) =>
  shape match {
    case Circle(_)       => true
    case Rectangle(_, _) => true
  }
}

prop.check()
// + OK, passed 100 tests.
Cogen for function generation
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0

import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.{Arbitrary, Cogen, Gen}

case class Point(x: Int, y: Int)

// Cogen lets ScalaCheck generate functions *from* your type
implicit val cogenPoint: Cogen[Point] = Cogen.derived[Point]

// Now ScalaCheck can generate Point => String functions
val genFn: Gen[Point => String] = Arbitrary.arbitrary[Point => String]
val fn = genFn.sample.get
println(fn(Point(1, 2)))
Shrink for minimal failing cases
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0

import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.Shrink

case class Config(retries: Int, timeout: Long, label: String)

implicit val shrinkConfig: Shrink[Config] = Shrink.derived[Config]

// Shrink produces smaller variants of a failing input
val original = Config(100, 5000L, "production")
val shrunk = Shrink.shrink(original).take(5).toList
shrunk.foreach(println)
Recursive data types
//> using scala 2.13.18
//> using dep com.kubuszok::kindlings-scalacheck-derivation:0.2.0
//> using dep org.scalacheck::scalacheck:1.19.0

import hearth.kindlings.scalacheckderivation._
import hearth.kindlings.scalacheckderivation.extensions._
import org.scalacheck.Arbitrary

case class Tree(value: Int, children: List[Tree])

// No Lazy wrapper needed -- recursion is handled automatically
implicit val arbTree: Arbitrary[Tree] = Arbitrary.derived[Tree]

// Recursive types may produce deep trees; retry if sample is None
val sample = Iterator.continually(Arbitrary.arbitrary[Tree].sample).flatten.next()
println(sample)

Debugging

Enable debug logging to see the derivation process:

import hearth.kindlings.scalacheckderivation.debug._

Or via scalac option:

-Xmacro-settings:scalacheckDerivation.logDerivation=true

Comparison with scalacheck-shapeless

Feature differences

Feature scalacheck-shapeless Kindlings
Same API on Scala 2.13 and 3 No (Scala 2 only) Yes
Arbitrary derivation Yes Yes
Cogen derivation Yes Yes
Shrink derivation No Yes
Recursive types Needs Lazy wrappers Just works
Scala 3 enums No Yes
Java enums No Yes
Named tuples No Yes
Opaque types No Yes
No shapeless dependency No Yes