Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Scala, how can I programmatically determine the name of the fields of a case class?

In Scala, suppose I have a case class like this:

case class Sample(myInt: Int, myString: String)

Is there a way for me to obtain a Seq[(String, Class[_])], or better yet, Seq[(String, Manifest)], describing the case class's parameters?

like image 537
Jean-Philippe Pellet Avatar asked Jun 08 '11 17:06

Jean-Philippe Pellet


People also ask

Which is a method that is generated for us in a case class?

Case class constructor parameters are public val fields by default, so accessor methods are generated for each parameter. An apply method is created in the companion object of the class, so you don't need to use the new keyword to create a new instance of the class.

What is case class in Scala syntax of case class?

A Case Class is just like a regular class, which has a feature for modeling unchangeable data. It is also constructive in pattern matching.

What is the case keyword 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.


2 Answers

I'm answering my own question to provide a base solution, but I'm looking for alternatives and improvements, too.


One option, also compatible with Java and not restricted to case classes, is to use ParaNamer. In Scala, another option is to parse the ScalaSig bytes attached to generated classfiles. Both solutions won't work in the REPL.

Here's my attempt at extracting the names of the fields from ScalaSig (which uses scalap and Scala 2.8.1):

def valNames[C: ClassManifest]: Seq[(String, Class[_])] = {
  val cls = classManifest[C].erasure
  val ctors = cls.getConstructors

  assert(ctors.size == 1, "Class " + cls.getName + " should have only one constructor")
  val sig = ScalaSigParser.parse(cls).getOrElse(error("No ScalaSig for class " + cls.getName + ", make sure it is a top-level case class"))

  val classSymbol = sig.parseEntry(0).asInstanceOf[ClassSymbol]
  assert(classSymbol.isCase, "Class " + cls.getName + " is not a case class")

  val tableSize = sig.table.size
  val ctorIndex = (1 until tableSize).find { i =>
    sig.parseEntry(i) match {
      case m @ MethodSymbol(SymbolInfo("<init>", owner, _, _, _, _), _) => owner match {
        case sym: SymbolInfoSymbol if sym.index == 0 => true
        case _ => false
      }
      case _ => false
    }
  }.getOrElse(error("Cannot find constructor entry in ScalaSig for class " + cls.getName))

  val paramsListBuilder = List.newBuilder[String]
  for (i <- (ctorIndex + 1) until tableSize) {
    sig.parseEntry(i) match {
      case MethodSymbol(SymbolInfo(name, owner, _, _, _, _), _) => owner match {
        case sym: SymbolInfoSymbol if sym.index == ctorIndex => paramsListBuilder += name
        case _ =>
      }
      case _ =>
    }
  }

  paramsListBuilder.result zip ctors(0).getParameterTypes
}

Disclaimer: I don't really understand the structure of ScalaSig and this should be considered as a heuristics. In particular, this code makes the following assumptions:

  • Case classes have only one constructor.
  • The entry of the signature at position zero is always a ClassSymbol.
  • The relevant constructor of the class is the first MethodEntry with name <init> whose owner has id 0.
  • The parameter names have as owner the constructor entry and always after that entry.

It will fail (because of no ScalaSig) on nested case classes.

This method also only returns Class instances and not Manifests.

Please feel free to suggest improvements!

like image 118
Jean-Philippe Pellet Avatar answered Nov 04 '22 02:11

Jean-Philippe Pellet


Here's a different solution that uses plain-Java reflection.

case class Test(unknown1: String, unknown2: Int)
val test = Test("one", 2)

val names = test.getClass.getDeclaredFields.map(_.getName)
// In this example, returns Array(unknown1, unknown2).

To get a Seq[(String, Class[_])], you can do this:

val typeMap = test.getClass.getDeclaredMethods.map({
                x => (x.getName, x.getReturnType)
              }).toMap[String, Class[_]]
val pairs = names.map(x => (x, typeMap(x)))
// In this example, returns Array((unknown1,class java.lang.String), (two,int))

I'm not sure about how to get Manifests.

like image 28
Jim Pivarski Avatar answered Nov 04 '22 04:11

Jim Pivarski