Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extracting and accessing fields at compile time in Scala 3

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.

like image 475
Koosha Avatar asked May 24 '21 21:05

Koosha


Video Answer


1 Answers

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
like image 60
gianluca aguzzi Avatar answered Oct 19 '22 17:10

gianluca aguzzi