ScalaではReflectionを使うとgenericsの型情報など詳細な型情報を取得することができます。
Scala2.10以前では、以下のように型情報を取得できます。
case class Person(id:Int, name:String, age:Option[Int])
val c = classOf[Person] // Class[Person]を取得
c.getSimpleName // Person
c.getName // Personクラスのパッケージ名を含むfull path
しかし、Person
クラスにどのような型の変数が定義されているか知りたい場合、
Class.getDeclaredFields/Methods
などなどの方法がありますが、どれも直接的でなくコーディングが大変でした。また、1や2の方法ではage:Option[Int]
などgenericなクラスの型パラメータ(この場合はInt
)までは取得できません。なぜなら、コンパイル後のバイトコードではOption[Int]
はOption[java.lang.Object]
と型情報を削られた形で表現されてしまうからです(type erasureと呼ばれます)。そのためJava(少なくともJava1.7の時点)ではOption[Int]
の型を正確にプログラム中から調べることは不可能でした。
このtype erasureを克服するため、Scalaではsignatureと呼ばれる情報がコンパイル後のクラスファイルにこっそり埋め込まれています。これにアクセスするのが3の方法ですが、Scalaの型の取り扱いについての深い知識が要求され、すぐに使いこなすのは難しいでしょう。
Scala2.10ではTypeTagが導入されsignatureへのアクセスが比較的容易になりました。
reflectの機能はScalaの本体とは別になっているので、sbtのlibraryDependencies
に以下の設定を追加します。
"org.scala-lang" % "scala-reflect" % "2.10.0"
case class Person(id:Int, name:String, age:Option[Int])
を定義して、パラメータの型情報を取り出します。
def getType[A : TypeTag](obj:A) : Type = typeOf[A]
のように書くと、コンパイラがobj:A
の型情報(TypeTag)を生成し、typeOf[A]
でコード中に型情報を取り出せるようになります。
// この2行でScala2.10のreflectionの機能が使えるようになる
import scala.reflect.runtime.{universe => ru}
import ru._
object ExtractTypeInfo extends Logger {
// 任意のオブジェクトからTypeTagを取得。取得できない場合はコンパイルエラーになる
def getType[A : TypeTag](obj:A) : Type = typeOf[A]
// TypeからClass[_]情報を取得するためのミラー
val mirror = ru.runtimeMirror(Thread.currentThread.getContextClassLoader)
// Type情報を再帰的に解決
def resolveType[T](tpe:T) : String = tpe match {
// TypeRefから型情報を抜き出す
case tr @ TypeRef(prefix, symbol, typeArgs) =>
// Typeに対応するClassを取得
val cl = mirror.runtimeClass(tr)
var className =
if(typeArgs.isEmpty)
cl.getSimpleName
else // 型パラメータを持っている場合、各パラメータの型を解決
s"${cl.getName}[${typeArgs.map(resolveType(_)).mkString(", ")}]"
// コンストラクタで定義されているパラメータを取得
val cc = tr.declaration(ru.nme.CONSTRUCTOR)
if(cc.isMethod) { // コンストラクタの有無をチェック
// コンストラクタの最初の括弧内のパラメータ情報を取り出す
val fstParen = cc.asMethod.paramss.headOption.getOrElse(Seq.empty)
val params = for(p <- fstParen) yield {
val name = p.name.decoded // パラメータ名を取得
val t = resolveType(p.typeSignature) // パラメータの型を取得し解決
s"$name:${t}"
}
if(!params.isEmpty)
className += s"(${params.mkString(", ")})"
}
className
}
case class Person(id:Int, name:String, age:Option[Int])
def main(args:Array[String]) {
val p = Person(1, "leo", None)
val tpe = getType(p)
val t = resolveType(tpe)
println(t)
}
}
Person(id:int, name:String, age:scala.Option[int])
上記のコードでは、Option[T]
の型パラメータまで調べることができ、ScalaのInt型などは実際にはJavaのprimitive型のintになっていることがわかります。
typeOf[A]
で取得したType
は、=:=
を使って以下のように比較できます。
val tpe = typeOf[Int]
tpe match {
case t if t =:= typeOf[Short] => "is short type"
case t if t =:= typeOf[Boolean] => "is boolean type"
case t if t =:= typeOf[Byte] => ...
case t if t =:= typeOf[Char] =>
case t if t =:= typeOf[Int] =>
case t if t =:= typeOf[Float] =>
case t if t =:= typeOf[Long] =>
case t if t =:= typeOf[Double] =>
case t if t =:= typeOf[String] =>
}
コンパイル時にTypeTagが得られない場合(例えばクラス名だけからオブジェクトを動的に作成する場合など)、Class[A]の情報からmirrorを経由してsignatureを取り出すこともできます。
val cl = classOf[Person]
val mirror = ru.runtimeMirror(Thread.currentThread.getContextClassLoader)
// クラス名からClassSymbol (Type)情報を取り出す
val classSymbol : ru.ClassSymbol = mirror.staticClass(cl.getCanonicalName)
// コンストラクタを調べる
val cc = classSymbol.typeSignature.declaration(ru.nme.CONSTRUCTOR)
val params = if(cc.isMethod) {
val fstParen = cc.asMethod.paramss.headOption.getOrElse(Seq.empty)
for(p <- fstParen) yield {
val name = p.name.decoded
val tpe = resolveType(p.typeSignature) // 上記のコードを呼び出す
s"$name:$tpe"
}
}
else Seq.empty
println(params.mkString(", ")) // id:int, name:String, age:scala.Option[int]
Scala2.10のreflectionの機能は強力ですが、上記のように再帰的な処理が必要となるなどやや不便なところがあります。これを解決するためにxerial-lens
というライブラリを作成しました。
sbtのlibraryDependencies
に、
"org.xerial" % "xerial-lens" % "3.1"
を追加すると、型情報を取り出すObjectTypeが使えるようになります。
内部ではTypeTagだけではなくScalaSigにアクセスして詳細な型情報を取り出すなどの工夫がされています。
import xerial.lens.{ObjectType,StandardType,Primitive, MapType}
val ot = ObjectType(classOf[Person])
println(ot) // Person
// パターンマッチで型による場合分けが可能
val params = ot match {
case s @ StandardType(cl) => s.constructorParams.mkString(", ")
case Primitive.Int => "int type"
case m @ MapType(cl, keyType, valueType) => s"Map[$keyType, $valueType]"
case _ => "no params"
}
println(params) // id:Int, name:String, age:Option[Int]
xerial-lensには、その他にもメソッドやアノテーションの情報を取り出すObjectSchemaや、それを利用してコマンドラインプログラムの作成を簡単にするLauncherなどが含まれています。