In a previous post I’ve shown how to build a minimal example of a random case class generator using the metaprogramming features of Scala 3. While investigating this topic, I naturally came upon multiple ways to do this. In this post I will elaborate on two other ways to build the same generator and pick my personal favorite.

The previous example

As a reference, here’s the final version of the previous posts solution[1][2]:

trait Generator[A]:
  def generate: A

object Generator:
  def create[A](a: => A) = new Generator[A]:
    override def generate: A = a

  // Simple instances needed for all methods used in this post
  given int: Generator[Int] = create(Random.nextInt())
  given double: Generator[Double] = create(Random.nextDouble())
  given bool: Generator[Boolean] = create(Random.nextBoolean())

  private val lorumWords = "lorum ipsum, etc.".split("[.|,]? ")
  given string: Generator[String] = create(lorumWords(Random.between(0, lorumWords.length)))


  // Specific code for this method below
  given empty: Generator[EmptyTuple] = create(EmptyTuple)

  given tuple[H, T <: Tuple](using hGen: Generator[H], tGen: Generator[T]): Generator[H *: T] =
    create(hGen.generate *: tGen.generate)

  given product[P <: Product](using m: Mirror.ProductOf[P], ts: Generator[m.MirroredElemTypes]): Generator[P] = create {
    val representation = ts.generate
    m.fromProduct(representation)
  }

Above code shows how we would have solved generic derivation using the shapeless library in scala 2.x (here replaced with scala 3 constructs[3]). It is based on implicit methods called recursively until a Generator for each field is found. Only then will this code compile when asked for a Generator.

A Scala 3 alternative

Scala 3 now contains all the tools to build this Generator without using an external library like shapeless. Moreover, it also provides us with more advanced tools like inlining, and compile time type matching. Previously, we’ve seen that a case class can be described as a N-arity Tuple like (N1, N2, …​, N), or equivalent, a recursive structure N1 *: N2 *: .. *: N *: EmptyTuple. What Scala 3 adds to its metaprogramming tools is the ability to match on these Tuples. To use this at compile time, the compiler needs to 1. be able to inline our code 2. cope with type erasure by calling erasedValue on the Type we need.

object Hello:
  inline def hello[A]: Unit =
    inline erasedValue[A] match
      case _: Int      => println("Hello Int")
      case _: String   => println("Hello String")
      case _: (tpe *: types) => hello[tpe]; hello[types]
      case EmptyTuple  => println("That's it")

  hello[(String, Int)]

When ran, this code will print:

Hello String
Hello Int
Thats it

What’s so special about this, is the matching clause on case _: (tpe *: types), where tpe and types are the types matched in the Tuple, not its values, and may be used as type parameters again.

When building our generator, we could use this construct again to recursively summon a Generator for these types using another inline construct: summonInline:

// General trait, object and instances as shown above

// Specific code for this method below
  inline def tuple[Types <: Tuple](ts: Tuple): Tuple =
    inline erasedValue[Types] match
      case EmptyTuple => ts
      case _: (tpe *: types) =>
        val value  = summonInline[Generator[tpe]].generate
        val values = ts :* value
        tuple[types](values)

This way of building the Generator[Tuple] removes some of the magic of resolving implicits, that we needed in the shapeless-like example above. summonInline does exactly as it promises, it either finds a Generator in scope for the first type of the matched Tuple tpe, or fails to compile.

Scala 3 adds one other feature for auto-deriving a Generator type-class, however. If we add a method called derived, we’re able to use the clause derives to derive a Generator[Person] in its companion object for free!

  inline def derived[P <: Product](using m: Mirror.ProductOf[P]): Generator[P] = new Generator[P]:
    def generate: P =
      val representation = tuple[m.MirroredElemTypes](EmptyTuple)
      m.fromProduct(representation)

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

Again, we use the Mirror of Product P to generate its representation, and generate a P from it using said Mirror.

The full example can be found here.

Magnolia for better ergonomics and readability

Recently I watched a very inspiring Zymposium video by Kit Langton, who showed a promising method of deriving a generic Differ for case classes using magnolia. Using the library looked so simple, I had to try that on top of the vanilla Scala 3 method to create our Generator.

Magnolia offers a macro for type-class derivation, listed on their Github. There are two methods we need complete for a full example. However, that also covers sum-types, which for this purpose we’re not interested in, yet. Let’s complete the example for our Generator:

object Generator:
  def join[T](ctx: CaseClass[Generator, T]): Generator[T] = new Generator[T]:
    override def generate: T = ctx.construct(param => param.typeclass.generate)

  // instances for simple types go here

That’s right, this became a one-liner using magnolia, when we forget the simple instances we also needed for the other examples. Magnolia’s macro looks for the method join, that needs a CaseClass context for our type-class to derive. Similar to Mirror.fromProduct before, the CaseClass context is able to create case classes from their representation.

Magnolia however paved the way for us by providing the parameter we are going to generate a value for. That parameter contains a typeclass field to access a Generator in scope, much like summonInline. The CaseClass context loops over each parameter of the case class under construction, and our Generator does the rest.[4].

Since the Product case is so simple, let’s also cover the sum-type case now, to generate sealed traits as well.

object Generator:
  override def split[T](ctx: SealedTrait[Generator, T]): Generator[T] = new Generator[T]:
    override def generate: T =
      val subs = ctx.subtypes
      val pick = Random.between(0, subs.size)
      ctx.subtypes.apply(pick).typeclass.generate

  // instances for simple types go here

The SealedTrait context provides us with all the subtypes we need to pick from with subtypes. We then roll a dice and pick one, ask for the typeclass like we did in the Product case, and generate it. If that subtype is a Product, the join method above provides all means to generate it, and vice versa, when a parameter of a Product is a sealed trait.

Finally, Magnolia adds an AutoDerivation trait to add an instance of Generator to our case class (providing a def derived):

object Generator extends AutoDerivation[Generator]:

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

Conclusion

Magnolia offers a great way to start generating your own type-classes. Even though Scala 3 adds metaprogramming constructs out of the box, the ease-of-use and readability the library adds to vanilla Scala makes this easier and more understandable even for less experienced programmers.

Code

The code for this blog can be found on Gitlab


1. this time using the new Scala 3 indentation based syntax
2. I listed the bare minimum and left convenience methods out.
3. for an example in scala 2.x using shapeless, see resources 1, 2 and 3
4. magnolia is more powerful, but we might explore that in the future
shadow-left