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