All the documentation and stack overflow says that ‘Whitebox’ macros are impossible in Scala 3.
In fact, by making a transparent
macro that returns a structural type you can create types with any val
s and def
s that you wish.
Here’s an example:
case class User(firstName: String, age: Int)
// has the type of Props { val firstName: String }
val userProps = props[User]
println(userProps.firstName) // prints "prop for firstName"
println(userProps.lastName) // compile error
Here’s the implementation:
import scala.compiletime.*
import scala.quoted.*
import scala.deriving.Mirror
class Props extends Selectable:
def selectDynamic(name: String): Any =
"prop for " + name
transparent inline def props[T] =
${ propsImpl[T] }
private def propsImpl[T: Type](using Quotes): Expr[Any] =
import quotes.reflect.*
Expr.summon[Mirror.ProductOf[T]].get match
case '{ $m: Mirror.ProductOf[T] {type MirroredElemLabels = mels; type MirroredElemTypes = mets } } =>
Type.of[mels] match
case '[mel *: melTail] =>
val label = Type.valueOfConstant[mel].get.toString
Refinement(TypeRepr.of[Props], label, TypeRepr.of[String]).asType match
case '[tpe] =>
val res = '{
val p = Props()
p.asInstanceOf[tpe]
}
println(res.show)
res
Many thanks to Guillaume Martres for help with getting the type tpe
from Refinement
… that was tricky. I looked at the generated code and can confirm reflection is only ever at compile time.
You could potentially use this approach for a wide variety of purposes, for example I made a SQL ‘specification’ helper:
case class Car(id: Int|Null = null, model: String, topSpeed: Int)
val cols = columns[Car]
val spec = SpecBuilder()
.where(cols.model.notEq("Lambo"))
.where(cols.topSpeed.gt(100) or cols.model.eq("Corvette"))
.orderBy(cols.topSpeed.desc)
.offset(1)
.build()
findAll(spec).mkString(", ")
But will an IDE ever support auto-completion for this? I have no clue.