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.
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:
builderImpl summons the product mirror:
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.
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