Extracting and accessing fields at compile time in Scala 3
Asked Answered
H

1

13

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.

Haemostasis answered 24/5, 2021 at 21:25 Comment(2)
It's not clear to me why using 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.Dovetail
When I decompile a case class, productElement is implemented using if-then-else, and productIterator is implemented by calling productElement 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
A
3

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
Amena answered 25/5, 2021 at 10:29 Comment(6)
Thank you @gianluca for your solution. Yes, it works, but I would argue that it is not 100% correct. Here is why: Imagine instead of println I wanted to call pp. There are 3 pp methods. 1st is def pp(value: Any): Unit = println(value). 2nd and 3rd are the same, except for value: Int and value: String. Now if I write pp(abc.name) the one with value: String will get called. But in your solution, value: Any will get called. Any idea how to fix this?Haemostasis
To make it even more specific, when I write pp(abc.name) and pp(abc.age), I don't even need pp(value: Any): Unit = println(value) to be present. But in the Macro version, with that overload the code won't compile.Haemostasis
Thank you for your feedback :). I really appreciate that. Yeah, I see, sorry but I thought that you only need the macro for the println method. BTW I think that your problem can be solved using implicit summoning and the type classes (Show for example?) that consumes some type. If you like, I can extend the example with this insight :)Amena
Macro Free? Yes, please :)Haemostasis
@Haemostasis sorry but I can't resolve it using only derivation (with mirror) and type classes, probably I lack some information. I know that isn't always a good idea to use low-level API but I hope to help you in some way :)Amena
@gianlucaaguzzi can you explain why this fields.map(typeRep.memberType) .map(_.typeSymbol) .map(symbol => TypeIdent(symbol)) .map(_.tpe) .map(_.asType) is necessary? Why not call typeRep.memberType(field).asType directly? If the TypeIdent is necessary to get a reference to the type from outside the context of the owning class, couldn't we also use newOwnerTypeRepr.select(field).asType?Earthquake

© 2022 - 2024 — McMap. All rights reserved.