Scala 3 Macros: Create Types with Arbitrary Values & Methods :: Jun 17, 2023

Sometimes you want to make types whose values & methods are determined programmatically, at compile time.

An example usecase is a Builder for arbitrary case classes:

case class User(firstName: String, age: Int)

val userBuilder = Builder[User]
  .withFirstName("Athena")
  .withAge(22)

println(userBuilder.age) // 22
println(userBuilder.build) // User("Athena", 22)

Note that autocomplete is somewhat working in Metals:


And that the type of userBuilder is

Builder[User] {
  val firstName: java.lang.String
  def withFirstName(value: java.lang.String): this
  val age: scala.Int
  def withAge(value: scala.Int): this
}

Tools of the Trade

One way to implement the Builder would be with Macro Annotations, which are an experimental feature introduced in Scala 3.3.

But this is not required. Programatic Structural Types already let us refine types with new vals and defs. Then we just need a macro to decide the names of the vals & defs. The macro must also be transparent in order to return the programmatic type.

Implementing the Builder class

As with other Structural Types, our Builder class must implement Selectable.

class Builder[T](
    mirror: Mirror.ProductOf[T],
    getters: IArray[String],
    setters: IArray[String]
) extends Selectable:
  private val values = Array.ofDim[Object](getters.length)

  def selectDynamic(name: String): Any =
    values(getters.indexOf(name))

  def applyDynamic(name: String)(args: Any*): this.type =
    values(setters.indexOf(name)) = args.head.asInstanceOf[Object]
    this

  def build: T = mirror.fromProduct(Tuple.fromArray(values))

When we call Builder[User].age, this is translated by the compiler into Builder[User].selectDynamic("age"). Likewise, Builder[User].withAge(22) becomes Builder[User].applyDynamic("withAge")(22). Finally, build uses a Mirror to instantiate the class.

For Builder[User], the getters array must be IArray("firstName", "age"), and setters must be IArray("withFirstName", "withAge").

The Builder macro

Our macro is defined as the apply method in Builder’s companion object:

import scala.deriving.*
import scala.compiletime.*
import scala.quoted.*

object Builder:
  transparent inline def apply[T <: Product] = ${ builderImpl[T] }

builderImpl summons the product mirror:

private def builderImpl[T: Type](using Quotes): Expr[Any] =
  Expr.summon[Mirror.ProductOf[T]].get match
    case '{
          $m: Mirror.ProductOf[T] {
            type MirroredElemLabels = mels
            type MirroredElemTypes = mets
          }
        } =>
      refineBuilder[T, mets, mels, Builder[T]](m)

refineBuilder is the meat and potatoes. It recursively loops over T’s element types & labels, constructing the getter and setter arrays. Refinement is used to construct the structural refinement on Builder[T]. For the getter, this refinement is a simple val (Refinement(TypeRepr.of[Res], getter, TypeRepr.of[met])). But setters are harder, since they are Methods returning this.type.

private def refineBuilder[T: Type, Mets: Type, Mels: Type, Res: Type](
    m: Expr[Mirror.ProductOf[T]],
    getters: IArray[String] = IArray.empty,
    setters: IArray[String] = IArray.empty
)(using Quotes): Expr[Any] =
  import quotes.reflect.*
  (Type.of[Mets], Type.of[Mels]) match
    case ('[met *: metTail], '[mel *: melTail]) =>
      val getter: String = Type.valueOfConstant[mel].get.toString
      val setter = "with" + getter.head.toUpper + getter.substring(1)
      val getterRefinement =
        Refinement(TypeRepr.of[Res], getter, TypeRepr.of[met])
      val setterRefinement = RecursiveType: parent =>
        val mt =
          MethodType(List("value"))(
            _ => List(TypeRepr.of[met]),
            _ => parent.recThis
          )
        Refinement(getterRefinement, setter, mt)
      setterRefinement.asType match
        case '[tpe] =>
          refineBuilder[T, metTail, melTail, tpe](
            m,
            getters :+ getter,
            setters :+ setter
          )
    case ('[EmptyTuple], '[EmptyTuple]) =>
      '{
        new Builder[T](
          $m,
          ${ Expr(getters) },
          ${ Expr(setters) }
        ).asInstanceOf[Res]
      }

Error Messages

Because the setter methods return this.type, compiling Builder[User].wrongMethodName(22) makes scalac stack overflow. Hopefully there’s a better way to encode the refinement with RecursiveType.

Conclusions

Scala 3 meta-programming is a pleasure to use. Macros + structural types allow many possibilities, but should only be used when all other methods are exhausted, since the IDE support and error messages are not as good as the alternatives.

Edit: It appears that returning this in a structural type is deprecated: https://github.com/lampepfl/dotty/discussions/14056#discussioncomment-6238099

Final Code

import scala.deriving.*
import scala.compiletime.*
import scala.quoted.*
import scala.collection.mutable as m
import scala.reflect.{ClassTag, classTag}

class Builder[T](
    mirror: Mirror.ProductOf[T],
    getters: IArray[String],
    setters: IArray[String]
) extends Selectable:
  private val values = Array.ofDim[Object](getters.length)

  def selectDynamic(name: String): Any =
    values(getters.indexOf(name))

  def applyDynamic(name: String)(args: Any*): this.type =
    values(setters.indexOf(name)) = args.head.asInstanceOf[Object]
    this

  def build: T = mirror.fromProduct(Tuple.fromArray(values))

object Builder:
  transparent inline def apply[T <: Product] = ${ builderImpl[T] }

  private def builderImpl[T: Type](using Quotes): Expr[Any] =
    Expr.summon[Mirror.ProductOf[T]].get match
      case '{
            $m: Mirror.ProductOf[T] {
              type MirroredElemLabels = mels
              type MirroredElemTypes = mets
            }
          } =>
        refineBuilder[T, mets, mels, Builder[T]](m)

  private def refineBuilder[T: Type, Mets: Type, Mels: Type, Res: Type](
      m: Expr[Mirror.ProductOf[T]],
      getters: IArray[String] = IArray.empty,
      setters: IArray[String] = IArray.empty
  )(using Quotes): Expr[Any] =
    import quotes.reflect.*
    (Type.of[Mets], Type.of[Mels]) match
      case ('[met *: metTail], '[mel *: melTail]) =>
        val getter: String = Type.valueOfConstant[mel].get.toString
        val setter = "with" + getter.head.toUpper + getter.substring(1)
        val getterRefinement =
          Refinement(TypeRepr.of[Res], getter, TypeRepr.of[met])
        val setterRefinement = RecursiveType: parent =>
          val mt =
            MethodType(List("value"))(
              _ => List(TypeRepr.of[met]),
              _ => parent.recThis
            )
          Refinement(getterRefinement, setter, mt)
        setterRefinement.asType match
          case '[tpe] =>
            refineBuilder[T, metTail, melTail, tpe](
              m,
              getters :+ getter,
              setters :+ setter
            )
      case ('[EmptyTuple], '[EmptyTuple]) =>
        '{
          new Builder[T](
            $m,
            ${ Expr(getters) },
            ${ Expr(setters) }
          ).asInstanceOf[Res]
        }