Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Obtaining the full info about fields of the case class in Scala

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.

like image 580
alex Avatar asked Feb 15 '17 10:02

alex


People also ask

What is the case class in Scala?

Scala case classes are just regular classes which are immutable by default and decomposable through pattern matching. It uses equal method to compare instance structurally. It does not use new keyword to instantiate object. All the parameters listed in the case class are public and immutable by default.

For which kind of data should you use a case class in Scala?

A Scala Case Class is like a regular class, except it is good for modeling immutable data. It also serves useful in pattern matching, such a class has a default apply() method which handles object construction. A scala case class also has all vals, which means they are immutable.

What methods get generated when we declare a case class?

equals and hashCode methods are generated, which let you compare objects and easily use them as keys in maps.

What is difference between class and case class in Scala?

A class can extend another class, whereas a case class can not extend another case class (because it would not be possible to correctly implement their equality).


1 Answers

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.

like image 127
Travis Brown Avatar answered Nov 13 '22 22:11

Travis Brown