Extracting names and types of elements of a case class at compile time in Scala 3 has been already explained well in this blog: https://blog.philipp-martini.de/blog/magic-mirror-scala3/
However, the same blog uses productElement
to get the values stored in an instance. My question is how to access them directly? Consider the following code:
case class Abc(name: String, age: Int)
inline def printElems[A](inline value: A)(using m: Mirror.Of[A]): Unit = ???
val abc = Abc("my-name", 99)
printElems(abc)
How to you (update the signature of printElems
and) implements printElems
so that printElems(abc)
will be expanded to something like this:
println(abc.name)
println(abc.age)
or at least this:
println(abc._1())
println(abc._2())
But NOT this:
println(abc.productElement(0))
println(abc.productElement(1))
Needless to say that I am looking for a solution that works for arbitrary case classes and not just for Abc
. Also, if macros have to be used, then that is fine. But only Scala 3 please.
I give you a solution leveraging qoutes.reflect
during macro expansion.
With qoutes.reflect is possible to inspect the expression passed. In our case, we want to found the field name in order to access it (for some information about the AST representation you can read the documentation here).
So, first of all, we need to build an inline def in order to expand expression with macros:
inline def printFields[A](elem : A): Unit = ${printFieldsImpl[A]('elem)}
In the implementation, we need to:
To access to object field (only for case classes) we can use the object Symbol
and then the method case fields
. It gives us a List
populate with the Symbol
name of each case fields.
Then, to access to fields, we need to use Select
(given by the reflection module). It accepts a term and the accessor symbol. So, for example, when we write something like that:
Select(term, field)
It is as writing in code something like that:
term.field
Finally, to print each field, we can leverage only the splicing. Wrapping up, the code that produces what you need could be:
import scala.quoted.*
def getPrintFields[T: Type](expr : Expr[T])(using Quotes): Expr[Any] = {
import quotes.reflect._
val fields = TypeTree.of[T].symbol.caseFields
val accessors = fields.map(Select(expr.asTerm, _).asExpr)
printAllElements(accessors)
}
def printAllElements(list : List[Expr[Any]])(using Quotes) : Expr[Unit] = list match {
case head :: other => '{ println($head); ${ printAllElements(other)} }
case _ => '{}
}
So, if you use it as:
case class Dog(name : String, favoriteFood : String, age : Int)
Test.printFields(Dog("wof", "bone", 10))
The console prints:
wof
bone
10
After the comment of @koosha, I tried to expand the example selecting method by field type. Again, I used macro (sorry :( ), I don't know a way to select attribute field without reflecting the code. If there are some tips are welcome :)
So, in addition to the first example, in this case, I use explicit type classes summoning and type from the field.
I created a very basic type class:
trait Show[T] {
def show(t : T) : Unit
}
And some implementations:
implicit object StringShow extends Show[String] {
inline def show(t : String) : Unit = println("String " + t)
}
implicit object AnyShow extends Show[Any] {
inline def show(t : Any) : Unit = println("Any " + t)
}
AnyShow
is considered as the fail-safe default, if no other implicit are in found during implicit resolution, I use it to print the element.
Field type can be get using TypeRep
and TypeIdent
val typeRep = TypeRepr.of[T]
val fields = TypeTree.of[T].symbol.caseFields
val fieldsType = fields.map(typeRep.memberType)
.map(_.typeSymbol)
.map(symbol => TypeIdent(symbol))
.map(_.tpe)
.map(_.asType)
Now, giving the field and leveraging Expr.summon[T], I can select what instance of Show
to use:
val typeMirror = TypeTree.of[T]
val typeRep = TypeRepr.of[T]
val fields = TypeTree.of[T].symbol.caseFields
val fieldsType = fields.map(typeRep.memberType)
.map(_.typeSymbol)
.map(symbol => TypeIdent(symbol))
.map(_.tpe)
.map(_.asType)
fields.zip(fieldsType).map {
case (field, '[t]) =>
val result = Select(expr.asTerm, field).asExprOf[t]
Expr.summon[Show[t]] match {
case Some(show) =>
'{$show.show($result)}
case _ => '{ AnyShow.show($result) }
}
}.fold('{})((acc, expr) => '{$acc; $expr}) // a easy way to combine expression
Then, you can use it as:
case class Dog(name : String, favoriteFood : String, age : Int)
printFields(Dog("wof", "bone", 10))
This code prints:
String wof
String bone
Any 10
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With