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?
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?
It's me again (two years later). Here's a different, different solution using Scala reflection. It is inspired by a blog post, which was itself inspired by a Stack Overflow exchange. The solution below is specialized to the original poster's question above.
In one compilation unit (a REPL :paste
or a compiled JAR), include scala-reflect
as a dependency and compile the following (tested in Scala 2.11, might work in Scala 2.10):
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
object CaseClassFieldsExtractor {
implicit def makeExtractor[T]: CaseClassFieldsExtractor[T] =
macro makeExtractorImpl[T]
def makeExtractorImpl[T: c.WeakTypeTag](c: Context):
c.Expr[CaseClassFieldsExtractor[T]] = {
import c.universe._
val tpe = weakTypeOf[T]
val fields = tpe.decls.collectFirst {
case m: MethodSymbol if (m.isPrimaryConstructor) => m
}.get.paramLists.head
val extractParams = fields.map { field =>
val name = field.asTerm.name
val fieldName = name.decodedName.toString
val NullaryMethodType(fieldType) = tpe.decl(name).typeSignature
q"$fieldName -> ${fieldType.toString}"
}
c.Expr[CaseClassFieldsExtractor[T]](q"""
new CaseClassFieldsExtractor[$tpe] {
def get = Map(..$extractParams)
}
""")
}
}
trait CaseClassFieldsExtractor[T] {
def get: Map[String, String]
}
def caseClassFields[T : CaseClassFieldsExtractor] =
implicitly[CaseClassFieldsExtractor[T]].get
And in another compilation unit (the next line in the REPL or code compiled with the previous as a dependency), use it like this:
scala> case class Something(x: Int, y: Double, z: String)
defined class Something
scala> caseClassFields[Something]
res0: Map[String,String] = Map(x -> Int, y -> Double, z -> String)
It seems like overkill, but I haven't been able to get it any shorter. Here's what it does:
caseClassFields
function creates an intermediate CaseClassFieldsExtractor
that implicitly comes into existence, reports its findings, and disappears.CaseClassFieldsExtractor
is a trait with a companion object that defines an anonymous concrete subclass of this trait, using a macro. It is the macro that can inspect your case class's fields because it has rich, compiler-level information about the case class.CaseClassFieldsExtractor
and its companion object must be declared in a previous compilation unit to the one that examines your case class so that the macro exists at the time you want to use it.WeakTypeTag
. This evaluates to a Scala structure with lots of pattern matching and no documentation that I could find.CaseClassFieldsExtractor
.caseClassFields
) without being called too early, when it's not yet defined.Any comments that could refine this solution or explain how exactly the "implicits" do what they do (or if they can be removed) are welcome.
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
.