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:
- get all fields in the object
- access to fields
- print each field
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
productIterator
is not ok for you? In the end this code is compiled, accessing item by its name or an index through the product iterator is kinda the same. – DovetailproductElement
is implemented usingif-then-else
, andproductIterator
is implemented by callingproductElement
at every step. I understand that this is a small overhead. But it is still a runtime overhead (abc.productElement(1)
does not get optimized). – Haemostasis