Reputation: 970
Consider the following class and method:
case class User(id: Long, name: String) {
private var foo = "Foo" // shouldn't be printed
val bar = "bar" // also shouldn't be printed
}
case class Message(id: Long, userId: Long, text: String)
def printInfo[E](o: E)(implicit tt: TypeTag[E]) = {
}
I want to make this method print the name, type and value for each of the field for any case class, i.e.
printInfo(User(1, "usr1")) // prints something like "(id, Long, 1), (name, String)"
printInfo(Message(1, 1, "Hello World")) // prints "(id, Long, 1), (userId, Long, 1), (text, String, "Hello World")"
Adding some custom annotations for fields is also considerable.
Upvotes: 14
Views: 2643
Reputation: 61646
Starting Scala 2.13
, case class
es (which are an implementation of Product
) are now provided with a productElementNames method which returns an iterator over their field's names.
Combined with productIterator:
// val user = User(1, "user")
(user.productElementNames zip user.productIterator)
.map { case (field, value) => (field, value.getClass.getSimpleName, value) }
.toList
// List[(String, String, Any)] = List((id,Long,1), (name,String,user))
This:
extracts field names (Iterator(id, name)
) using productElementNames
extracts field values (Iterator(1, user)
) using productIterator
zip
s field names with field values
extracts the class name of values
Upvotes: 5
Reputation: 139028
You could do this by inspecting the members listed by the type tag and reflecting using its mirror:
import scala.reflect.ClassTag
import scala.reflect.runtime.universe.TypeTag
def printInfo[A](a: A)(implicit tt: TypeTag[A], ct: ClassTag[A]): String = {
val members = tt.tpe.members.collect {
case m if m.isMethod && m.asMethod.isCaseAccessor => m.asMethod
}
members.map { member =>
val memberValue = tt.mirror.reflect(a).reflectMethod(member)()
s"(${ member.name }, ${ member.returnType }, $memberValue)"
}.mkString(", ")
}
Which would work like this:
scala> case class User(id: Long, name: String) {
| private var foo = "Foo" // shouldn't be printed
| val bar = "bar" // also shouldn't be printed
| }
defined class User
scala> case class Message(id: Long, userId: Long, text: String)
defined class Message
scala> printInfo(User(1, "usr1"))
res0: String = (name, String, usr1), (id, scala.Long, 1)
scala> printInfo(Message(1, 1, "Hello World"))
res1: String = (text, String, Hello World), (userId, scala.Long, 1), (id, scala.Long, 1)
(If you wanted Long
instead of scala.Long
it wouldn't be too hard to drop the prefix from the type you get from member.returnType
, but I'll leave that as an exercise for the reader.)
It's also not too hard to do this without any runtime reflection using Shapeless:
import shapeless.{ ::, HList, HNil, LabelledGeneric, Typeable, Witness }
import shapeless.labelled.FieldType
trait PrettyPrintable[A] {
def apply(a: A): List[(String, String, String)]
}
object PrettyPrintable {
implicit val hnilPrettyPrintable: PrettyPrintable[HNil] =
new PrettyPrintable[HNil] {
def apply(a: HNil): List[(String, String, String)] = Nil
}
implicit def hconsPrettyPrintable[K <: Symbol, H, T <: HList](implicit
kw: Witness.Aux[K],
ht: Typeable[H],
tp: PrettyPrintable[T]
): PrettyPrintable[FieldType[K, H] :: T] =
new PrettyPrintable[FieldType[K, H] :: T] {
def apply(a: FieldType[K, H] :: T): List[(String, String, String)] =
(kw.value.name, ht.describe, a.head.toString) :: tp(a.tail)
}
implicit def genPrettyPrintable[A, R <: HList](implicit
ag: LabelledGeneric.Aux[A, R],
rp: PrettyPrintable[R]
): PrettyPrintable[A] = new PrettyPrintable[A] {
def apply(a: A): List[(String, String, String)] = rp(ag.to(a))
}
def printInfo[A](a: A)(implicit pp: PrettyPrintable[A]) = pp(a).map {
case (memberName, memberType, memberValue) =>
s"($memberName, $memberType, $memberValue)"
}.mkString(", ")
}
And then:
scala> PrettyPrintable.printInfo(User(1, "usr1"))
res2: String = (id, Long, 1), (name, String, usr1)
scala> PrettyPrintable.printInfo(Message(1, 1, "Hello World"))
res3: String = (id, Long, 1), (userId, Long, 1), (text, String, Hello World)
Among other things this gives you the fields in declaration order, which I think should be possible with the type tag approach, but I avoid that API as often as I can, so off the top of my head I'm not sure.
Upvotes: 19