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?
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.
A Case Class is just like a regular class, which has a feature for modeling unchangeable data. It is also constructive in pattern matching.
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.
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.
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:
ClassSymbol
.MethodEntry
with name <init>
whose owner has id 0.It will fail (because of no ScalaSig
) on nested case classes.
This method also only returns Class
instances and not Manifest
s.
Please feel free to suggest improvements!
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With