Получение полной информации о полях класса case в Scala

Рассмотрим следующий класс и метод:

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]) = {

}

Я хочу, чтобы этот метод печатал имя, тип и значение для каждого поля для любого класса case, т.е.

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")"

Добавление некоторых пользовательских аннотаций для полей также значительно.

Ответ 1

Вы можете сделать это, проверив список, указанный тегом типа, и отразив его зеркало:

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(", ")
}

Что будет работать следующим образом:

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)

(Если вы хотели Long вместо scala.Long, было бы не так сложно отбросить префикс от типа, который вы получаете от member.returnType, но я оставлю это как упражнение для читателя.)

Также не слишком сложно сделать это без отражения во время выполнения с использованием 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(", ")
}

И затем:

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)

Кроме всего прочего, это дает вам поля в порядке декларации, которые, я думаю, должны быть возможны с помощью подхода типа тегов, но я избегаю этого API так часто, как я могу, поэтому с головы до головы я не уверен.