Automatic derivation of scalacheck Arbitrary
(and Cogen
and Shrink
)
instances for Scala 3.
This enables automatic derivation for enums, case classes and sealed traits.
Since version 0.6.0, an additional module also supports automatic derivation for literal and union types - see docs/extras.md for more.
This library supports regular scala on jvm, scala-js and scala-native. See version matrix for details on versions.
Add a (test-)dependency on this project to your project. For example, in a regular (jvm-targeting) sbt project:
libraryDependencies ++= Seq(
"io.github.martinhh" %% "scalacheck-derived" % "<version>" % "test"
)
There are three ways of using this library. Let's look at them by examples using the following (example) data structure:
enum Color:
case Red, Blue, Yellow
sealed trait LibItem
case class Magazine(title: String, issue: Int, color: Color) extends LibItem
case class Book(title: String, author: String, color: Color) extends LibItem
By importing io.github.martinhh.derived.scalacheck.given
, you enable implicit resolution of
derived instances so that you can do this:
import io.github.martinhh.derived.scalacheck.given
import org.scalacheck.Prop
Prop.forAll { (item: LibItem) =>
// your test using item here...
}
This will recursively derive any missing instances (for members of case classes & tuples and for subtypes of sealed traits & enums).
Note that while this is convenient, this style of "ad-hoc-derivation" comes with the risk of instance being derived redundantly, potentially leading to overhead in compilation times that could be avoided.
Alternatively, you can use io.github.martinhh.derived.scalacheck.deriveArbitrary
to explicitly
derive instances where you need them:
import io.github.martinhh.derived.scalacheck.deriveArbitrary
import org.scalacheck.Arbitrary
given arbLibItem: Arbitrary[LibItem] = deriveArbitrary
Just like the "fully implicit" approach, this will recursively derive any missing instances.
If you'd like more control over what is being derived, you can use
io.github.martinhh.derived.scalacheck.deriveArbitraryShallow
to explicitly
derive instances:
import io.github.martinhh.derived.scalacheck.deriveArbitraryShallow
import org.scalacheck.Arbitrary
given arbColor: Arbitrary[Color] = deriveArbitraryShallow
given arbLibItem: Arbitrary[LibItem] = deriveArbitraryShallow
deriveArbitraryShallow
will not derive missing instances for members of case classes & tuples. In the above example,
the line given arbColor: Arbitrary[Color] = deriveArbitraryShallow
is required because without a
given Arbitrary[Color]
in scope, derivation of Arbitrary[LibItem]
would fail.
Since there is a default/fallback (non-shrinking) Shrink
instance provided by scalacheck for any type and since
derivation may cause significant compile-time overhead, derivation of Shrink
instances is optional.
If you feel the need to derive Shrink
instances, you can do so by either importing
io.github.martinhh.derived.shrink.given
(for "fully implicit" derivation) or by using
io.github.martinhh.derived.shrink.deriveShrink
.
There are limitations to the provided mechanism:
While derivation for recursive structures is supported, the runtime-behavior of the derived generators may be less than ideal and may need further tweaking. See ./docs/recursive.md for details and recommended solutions.
The derivation mechanism uses inlining. Depending on the size of the data structure for which one
wants to derive an Arbitrary
, it is possible to hit the compiler's maximum limit for number of
successive inlines.
In that case, compilation will fail with a message like this one:
[error] | Maximal number of successive inlines (32) exceeded,
[error] | Maybe this is caused by a recursive inline method?
[error] | You can use -Xmax-inlines to change the limit.
Since version 0.7.0, the derivation mechanism was optimized to leverage certain changes that came to effect
with Scala 3.4.0 which will drastically reduce the number of recursive inlining,
e.g. allowing derivation for case classes with more than 32 members without raising -Xmax-inlines
.
You will benefit from these changes if you depend on version 0.7.0 or higher and use Scala 3.4.0 or higher.
If that does not help (or if you are not able to use those versions), there are two ways to handle this:
- use
-Xmax-inlines
compiler setting to increase the limit - instead of deriving an
Arbitrary
for the whole structure at once, derive "intermediate" instances for some of its members (and place them into implicit scope).
Additionally, there may be known bugs - you can check for those here.
The following table documents which versions of scalacheck, scala, scala-js and scala-native were used for each release:
scalacheck-derived | scalacheck | scala | scala-js | scala-native | java |
---|---|---|---|---|---|
0.9.0 | 1.18.1 | 3.3.6 | 1.19.0 | 0.5.8 | 11 |
0.8.2 | 1.18.1 | 3.3.5 | 1.18.2 | 0.5.7 | 11 |
0.8.1 | 1.18.1 | 3.3.5 | 1.18.2 | 0.5.7 | 11 |
0.8.0 | 1.18.1 | 3.3.5 | 1.18.2 | 0.5.7 | 8 |
0.7.1 | 1.18.1 | 3.3.5 | 1.18.2 | 0.5.6 | 8 |
0.7.0 | 1.18.1 | 3.3.5 | 1.18.2 | 0.5.6 | 8 |
0.6.0 | 1.18.1 | 3.3.5 | 1.18.2 | 0.5.6 | 8 |
0.5.0 | 1.18.0 | 3.3.1 | 1.16.0 | 0.5.1 | 8 |
0.4.2 | 1.17.0 | 3.3.1 | 1.13.2 | 0.4.15 | 8 |
0.4.1 | 1.17.0 | 3.3.0 | 1.13.0 | 0.4.14 | 8 |
0.4.0 | 1.17.0 | 3.3.0 | 1.13.0 | 0.4.14 | 17 |
0.3.0 | 1.17.0 | 3.3.0 | 1.13.0 | 0.4.14 | 17 |
0.2.0 | 1.17.0 | 3.2.2 | 1.13.0 | 0.4.12 | 17 |
0.1.0 | 1.17.0 | 3.2.2 | 1.13.0 | 0.4.12 | 17 |