12

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.

Pᴇʜ
  • 56,719
  • 10
  • 49
  • 73
Koosha
  • 1,492
  • 7
  • 19
  • 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. – Gaël J May 25 '21 at 06:35
  • 1
    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). – Koosha May 25 '21 at 06:41

1 Answers1

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
gianluca aguzzi
  • 1,734
  • 1
  • 10
  • 22
  • 1
    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? – Koosha May 25 '21 at 19:48
  • 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. – Koosha May 25 '21 at 19:50
  • 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 :) – gianluca aguzzi May 25 '21 at 21:36
  • Macro Free? Yes, please :) – Koosha May 25 '21 at 22:14
  • 1
    @Koosha 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 :) – gianluca aguzzi May 26 '21 at 10:38