CLOVER🍀

That was when it all began.

Scala 2.10.0 Scala Reflection

ホントは次は「Akka Actors now part of the distribution」のつもりだったんですけど、ちょっと触ってみたらScala Actorのマイグレーションというよりは、Akkaにどんどん踏み込んでいかざるをえなくなってきたので、ちょっと機会を改めることにしました。

Akka単独で勉強するか、次のScalaバージョンを待った方が良さそう?

というわけで、実験的機能に行きましょうってことで、Scala Reflectionです。
http://docs.scala-lang.org/overviews/reflection/overview.html

かなり難解。以前コンパイラプラグインを書いたことはありますが、あれと同じような雰囲気を味わいました。強力なAPIだとは思いますが、これをみんな使う気はあんまりしませんね…。

では、いってみましょう。

リフレクションの対象とするクラスとしては、こんなのを用意しました。

class Member(val name: String) {
  def this() = this("dummy")

  def this(name: String, age: Int) = {
    this(name)
    this.age = age
  }

  var age: Int = _
  private var counter: Int = _
  implicit val iv: String = "Implicit Value"

  def methodA(pre: String, suf: String): Unit = {
    counter += 1
    println(s"$pre$name$suf")
  }

  def methodB(pre: String, suf: String): String = {
    counter += 1
    s"$pre$name$suf"
  }

  override def toString(): String =
    s"Member name=$name, counter=$counter, iv=$iv"
}

かなり、わざとらしいです…。

クラスの定義を調べる

まずは、クラスに定義されているフィールドとかの定義を取得したりするところから。概要にも書かれていますが、以下のimport文を加えておきます。

import scala.reflect.runtime.universe

*ドキュメントでは、universeに対してruという別名を与えていますが、ここではuniverseのまま書いていきます

ここからのコードで見ることになるのは、以下のクラスとかトレイトなどだと思います。
Universe
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Universe
JavaUniverse
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.JavaUniverse
TypeTag
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.TypeTags$TypeTag
Type
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$Type
Symbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$Symbol
TermSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$TermSymbol
MethodSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$MethodSymbol
MemberScope
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Scopes$MemberScope
DefinitionsApi
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.StandardDefinitions$DefinitionsApi

概要に沿っていくと、最初にTypeTagを取得する関数を定義します。

  def getTypeTag[T: universe.TypeTag](targetClass: Class[T]): universe.TypeTag[T] =
    universe.typeTag[T]

TypeTagは、universe#typeTagから取得します。Context Bound、初めて使ったような…。

この関数を使用して、返ってきたTypeTagからTypeを取得します。

    val theType = getTypeTag(classOf[Member]).tpe

Typeは、TypeTag#tpeで取得します。

Typeからは、

theType.declarations

で、クラス自体に定義してあるフィールドやメソッドを取得できます。Class#getDeclaredMethodsみたいな感じです。ちょっと範囲は広いですが。
また、

theType.members

で、親クラスを含めた定義を取得できます。

個別に指定する場合は、こんな感じで。

val nameField = theType.declaration(universe.newTermName("name"))

もちろん、Type#memberでもいいですが。

Type#declarationsやType#membersの戻り値であるMemberScopeは、Symbolに対する絞り込みが可能です。

サンプルをいくつか。

    val theType = getTypeTag(classOf[Member]).tpe
    // クラス自身に定義されたメンバを取得
    println(s"typeTag.declarations => ${theType.declarations}")
    // 全メンバの取得
    println(s"typeTag.members => ${theType.members}")

    // valの取得
    val vals = theType.declarations.filter(_.asTerm.isVal)
    println(s"vals => $vals")

    // varの取得
    val vars = theType.declarations.filter(_.asTerm.isVar)
    println(s"vars => $vars")

    // nameフィールドの取得
    val nameField = theType.declaration(universe.newTermName("name"))
    println(s"nameField => $nameField")

    // メソッドの取得
    val methods = theType.declarations.filter(_.isMethod)
    println(s"methods => $methods")
    methods foreach { m => println(s"method name = [${m.name.decoded}]") }

    // implicitが付与されたメンバの取得
    val implicits = theType.declarations.filter(_.isImplicit)
    println(s"implicits => $implicits")

    // プリミティブの取得
    val primitives = theType.declarations.filter(isPrimitive)
    println(s"primitives => $primitives")

プリミティブの判定は、簡単にはいかなさそうだったのでこんなのを書きました。

  def isPrimitive(symbol: universe.Symbol): Boolean =
    if (symbol.isMethod) {
      val rt = symbol.asMethod.returnType
      if (rt =:= universe.definitions.BooleanTpe) true
      else if (rt =:= universe.definitions.ByteTpe) true
      else if (rt =:= universe.definitions.CharTpe) true
      else if (rt =:= universe.definitions.DoubleTpe) true
      else if (rt =:= universe.definitions.FloatTpe) true
      else if (rt =:= universe.definitions.IntTpe) true
      else if (rt =:= universe.definitions.LongTpe) true
      else if (rt =:= universe.definitions.ShortTpe) true
      else false
    } else false

で、これを実行するとこんな出力が得られます。

typeTag.declarations => Scope{
  val name: <?>;
  private[this] val name: <?>;
  def <init>: <?>;
  def <init>: <?>;
  def <init>: <?>;
  def age: <?>;
  def age_=: <?>;
  private[this] var age: <?>;
  private def counter: <?>;
  private def counter_=: <?>;
  private[this] var counter: <?>;
  implicit val iv: <?>;
  private[this] val iv: <?>;
  def methodA: <?>;
  def methodB: <?>;
  override def toString: <?>
}
typeTag.members => Scope{
  override def toString(): String;
  def methodB(pre: <?>,suf: <?>): String;
  def methodA(pre: <?>,suf: <?>): scala.Unit;
  private[this] val iv: String;
  implicit val iv: String;
  private[this] var counter: scala.Int;
  private def counter_=(x$1: <?>): scala.Unit;
  private def counter: scala.Int;
  private[this] var age: scala.Int;
  def age_=(x$1: <?>): scala.Unit;
  def age: scala.Int;
  def <init>(name: <?>,age: <?>): Member;
  def <init>(): Member;
  def <init>(name: String): Member;
  private[this] val name: String;
  val name: String;
  final def $asInstanceOf[T0](): T0;
  final def $isInstanceOf[T0](): Boolean;
  final def synchronized[T0](x$1: T0): T0;
  final def ##(): Int;
  final def !=(x$1: AnyRef): Boolean;
  final def ==(x$1: AnyRef): Boolean;
  final def ne(x$1: AnyRef): Boolean;
  final def eq(x$1: AnyRef): Boolean;
  final def notifyAll(): Unit;
  final def notify(): Unit;
  protected def clone(): Object;
  final def getClass(): Class[_];
  def hashCode(): Int;
  def equals(x$1: Any): Boolean;
  final def wait(): Unit;
  final def wait(x$1: Long): Unit;
  final def wait(x$1: Long,x$2: Int): Unit;
  protected def finalize(): Unit;
  final def asInstanceOf[T0]: T0;
  final def isInstanceOf[T0]: Boolean;
  final def !=(x$1: Any): Boolean;
  final def ==(x$1: Any): Boolean
}
vals => Scope{
  private[this] val name: String;
  private[this] val iv: String
}
vars => Scope{
  private[this] var age: scala.Int;
  private[this] var counter: scala.Int
}
nameField => value name
methods => Scope{
  val name: String;
  def <init>(name: String): Member;
  def <init>(): Member;
  def <init>(name: String,age: scala.Int): Member;
  def age: scala.Int;
  def age_=(x$1: scala.Int): scala.Unit;
  private def counter: scala.Int;
  private def counter_=(x$1: scala.Int): scala.Unit;
  implicit val iv: String;
  def methodA(pre: String,suf: String): scala.Unit;
  def methodB(pre: String,suf: String): String;
  override def toString(): String
}
method name = [name]
method name = [<init>]
method name = [<init>]
method name = [<init>]
method name = [age]
method name = [age_=]
method name = [counter]
method name = [counter_=]
method name = [iv]
method name = [methodA]
method name = [methodB]
method name = [toString]
implicits => Scope{
  implicit val iv: String
}
primitives => Scope{
  def age: scala.Int;
  private def counter: scala.Int
}

ちなみに、TypeTagって使ってないよね?という話もあるかと思いますが、最初の

  def getTypeTag[T: universe.TypeTag](targetClass: Class[T]): universe.TypeTag[T] =
    universe.typeTag[T]

val theType = getTypeTag(classOf[Member]).tpe

  def getType[T: universe.TypeTag](targetClass: Class[T]): universe.Type =
    universe.typeOf[T]

val theType = getType(classOf[Member])

という置き換えが可能です。

というわけで、まとめると

scala.reflect.api.Universe

から

scala.reflect.api.Types.Type

を取得して、そこから欲しいメンバの定義を取得していこうという話になります。

各定義の判定には

scala.reflect.api.Symbols.Symbol

を使用します。Symbolには、MethodSymbolやTermSymbolなどがありますが、as[Type名]で変換可能です。変換可能かどうかは、is[Type名]で判定できます。

先の例だと、

    // valの取得
    val vals = theType.declarations.filter(_.asTerm.isVal)
    println(s"vals => $vals")

などでSymbolを1度TermSymbolに変換してから、valかどうかを判定していたりします。

インスタンスの生成とフィールドからの値取得・設定、メソッド呼び出し

続いて、今度はインスタンスの生成とそのインスタンスを使ったメソッド呼び出しなどをやってみます。

先ほど使用したUniverseやTypeなどに加えて、今度は以下のクラスなども使用します。
Mirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirror
ClassMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$ClassMirror
InstanceMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$InstanceMirror
FieldMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$FieldMirror
MethodMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Mirrors$MethodMirror
ClassSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$ClassSymbol
TermNamesApi
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.StandardNames$TermNamesApi

要は、Mirrorを使用しますよ、と。

では、インスタンスの生成からいってみましょう。

こんなシグネチャを持つ、newInstance関数の実装をしていきます。

  def newInstance[T: universe.TypeTag](targetClass: Class[T], args: Any*): T 

Mirrorは、やっぱりUniverseから取得できます。

    val mirror = universe.runtimeMirror(targetClass.getClassLoader)
    // これでもOK
    // val mirror = universe.typeTag[T].mirror

続いて、ClassSymbol、ClassMirror、そしてコンストラクタを表すMethodSymbolとMethodMirrorを取得します。

    val classSymbol = universe.typeOf[T].typeSymbol.asClass

    val classMirror = mirror.reflectClass(classSymbol)
    val constructorSymbol = universe.typeOf[T].declaration(universe.nme.CONSTRUCTOR).filter {
      _.asMethod.paramss match {
        case List(Nil) => args.isEmpty
        case List(List(as @ _*)) => as.size == args.size
      }
    }.asMethod
    val constructorMethodMirror = classMirror.reflectConstructor(constructorSymbol)

MethodMirror#applyを呼び出すことで、メソッド呼び出しが可能です。

    constructorMethodMirror(args: _*).asInstanceOf[T]

これで、インスタンスの生成ができます。

では、動かしてみましょう。

    println(newInstance(classOf[Member]))

    val m = newInstance(classOf[Member], "Taro")
    m.methodA("[", "]")

結果…わかりにくい例で、大変申し訳なく…。

Member name=dummy, counter=0, iv=Implicit Value
[Taro]

まあ、普通にインスタンス生成ができてインスタンスに対してメソッド呼び出しもできてますね?コンストラクタに引数を渡せてますね?ってことで。

ちなみに、

    val constructorSymbol = universe.typeOf[T].declaration(universe.nme.CONSTRUCTOR).filter {
      _.asMethod.paramss match {
        case List(Nil) => args.isEmpty
        case List(List(as @ _*)) => as.size == args.size
      }
    }.asMethod

でコンストラクタの選り分けをしていたわけですが、よくよく見ると引数の型チェックをまったくしていません。数しか見ていない、と。えらい端折ってます。

これ、できるのかなぁ?
なんとかなりました

では、フィールドからの値の取得・設定とメソッド呼び出しを一気に。

  def getFieldValue[A: universe.TypeTag: scala.reflect.ClassTag, B](instance: A, name: String): B = {
    val mirror = universe.runtimeMirror(instance.getClass.getClassLoader)
    val termSymbol = universe.typeOf[A].declaration(universe.newTermName(name)).asTerm

    val instanceMirror = mirror.reflect(instance)
    instanceMirror.reflectField(termSymbol).get.asInstanceOf[B]
  }

  def setFieldValue[A: universe.TypeTag: scala.reflect.ClassTag, B](instance: A, name: String, value: B):Unit = {
    val mirror = universe.runtimeMirror(instance.getClass.getClassLoader)
    val termSymbol = universe.typeOf[A].declaration(universe.newTermName(name)).asTerm

    val instanceMirror = mirror.reflect(instance)
    instanceMirror.reflectField(termSymbol).set(value)
  }

  def invokeMethod[A: universe.TypeTag: scala.reflect.ClassTag, B](instance: A, name: String, args: Any*): B = {
    val mirror = universe.runtimeMirror(instance.getClass.getClassLoader)
    val methodSymbol = universe.typeOf[A].declaration(universe.newTermName(name)).asMethod

    val instanceMirror = mirror.reflect(instance)
    instanceMirror.reflectMethod(methodSymbol)(args: _*).asInstanceOf[B]
  }

まずはMirrorの取得、そして自分が欲しい対象のSymbolを探します。そして、InstanceMirrorを取得した後にInstanceMirror#reflectFieldやInstanceMirror#reflectMethodで、フィールドやメソッド呼び出しを行っています。

実行例。

    val m = newInstance(classOf[Member], "Taro")
    m.methodA("[", "]")
    println("getFieldValue => " + getFieldValue(m, "name"))
    setFieldValue(m, "age", 25)
    println(s"setFieldValue age => ${m.age}")
    val result: String = invokeMethod(m, "methodB", "***", "***")
    println(s"invokeMethod => $result")

結果。

[Taro]
getFieldValue => Taro
setFieldValue age => 25
invokeMethod => ***Taro***

インスタンス生成の部分は余分ですが…。

とまあ、そこそこ触ってみましたが、疲れました…。そんなに情報無いですしね。チュートリアルと、参考サイトを見ながら頑張ってみました。

さて、このリフレクション、どう活用するのがいいのかな?

参考:
http://blob.geishatokyo.com/archives/153379

最後に、今回作成したコードを貼っておきます。

import scala.reflect.runtime.universe

object ScalaReflection {
  def main(args: Array[String]): Unit = {
    //val theType = getTypeTag(classOf[Member]).tpe
    val theType = getType(classOf[Member])
    println(s"typeTag.declarations => ${theType.declarations}")
    println(s"typeTag.members => ${theType.members}")

    val vals = theType.declarations.filter(_.asTerm.isVal)
    println(s"vals => $vals")

    val vars = theType.declarations.filter(_.asTerm.isVar)
    println(s"vars => $vars")

    val nameField = theType.declaration(universe.newTermName("name"))
    println(s"nameField => $nameField")

    val methods = theType.declarations.filter(_.isMethod)
    println(s"methods => $methods")
    methods foreach { m => println(s"method name = [${m.name.decoded}]") }

    val implicits = theType.declarations.filter(_.isImplicit)
    println(s"implicits => $implicits")

    val primitives = theType.declarations.filter(isPrimitive)
    println(s"primitives => $primitives")

    println(newInstance(classOf[Member]))

    val m = newInstance(classOf[Member], "Taro")
    m.methodA("[", "]")
    println("getFieldValue => " + getFieldValue(m, "name"))
    setFieldValue(m, "age", 25)
    println(s"setFieldValue age => ${m.age}")
    val result: String = invokeMethod(m, "methodB", "***", "***")
    println(s"invokeMethod => $result")
  }

  def getTypeTag[T: universe.TypeTag](targetClass: Class[T]): universe.TypeTag[T] =
    universe.typeTag[T]

  def getType[T: universe.TypeTag](targetClass: Class[T]): universe.Type =
    universe.typeOf[T]

  def isPrimitive(symbol: universe.Symbol): Boolean =
    if (symbol.isMethod) {
      val rt = symbol.asMethod.returnType
      if (rt =:= universe.definitions.BooleanTpe) true
      else if (rt =:= universe.definitions.ByteTpe) true
      else if (rt =:= universe.definitions.CharTpe) true
      else if (rt =:= universe.definitions.DoubleTpe) true
      else if (rt =:= universe.definitions.FloatTpe) true
      else if (rt =:= universe.definitions.IntTpe) true
      else if (rt =:= universe.definitions.LongTpe) true
      else if (rt =:= universe.definitions.ShortTpe) true
      else false
    } else false

  def newInstance[T: universe.TypeTag](targetClass: Class[T], args: Any*): T = {
    val mirror = universe.runtimeMirror(targetClass.getClassLoader)
    // これでもOK
    // val mirror = universe.typeTag[T].mirror
    val classSymbol = universe.typeOf[T].typeSymbol.asClass

    val classMirror = mirror.reflectClass(classSymbol)
    val constructorSymbol = universe.typeOf[T].declaration(universe.nme.CONSTRUCTOR).filter {
      _.asMethod.paramss match {
        case List(Nil) => args.isEmpty
        case List(List(as @ _*)) => as.size == args.size
      }
    }.asMethod
    val constructorMethodMirror = classMirror.reflectConstructor(constructorSymbol)
    constructorMethodMirror(args: _*).asInstanceOf[T]
  }

  def getFieldValue[A: universe.TypeTag: scala.reflect.ClassTag, B](instance: A, name: String): B = {
    val mirror = universe.runtimeMirror(instance.getClass.getClassLoader)
    val termSymbol = universe.typeOf[A].declaration(universe.newTermName(name)).asTerm

    val instanceMirror = mirror.reflect(instance)
    instanceMirror.reflectField(termSymbol).get.asInstanceOf[B]
  }

  def setFieldValue[A: universe.TypeTag: scala.reflect.ClassTag, B](instance: A, name: String, value: B):Unit = {
    val mirror = universe.runtimeMirror(instance.getClass.getClassLoader)
    val termSymbol = universe.typeOf[A].declaration(universe.newTermName(name)).asTerm

    val instanceMirror = mirror.reflect(instance)
    instanceMirror.reflectField(termSymbol).set(value)
  }

  def invokeMethod[A: universe.TypeTag: scala.reflect.ClassTag, B](instance: A, name: String, args: Any*): B = {
    val mirror = universe.runtimeMirror(instance.getClass.getClassLoader)
    val methodSymbol = universe.typeOf[A].declaration(universe.newTermName(name)).asMethod

    val instanceMirror = mirror.reflect(instance)
    instanceMirror.reflectMethod(methodSymbol)(args: _*).asInstanceOf[B]
  }
}

class Member(val name: String) {
  def this() = this("dummy")

  def this(name: String, age: Int) = {
    this(name)
    this.age = age
  }

  var age: Int = _
  private var counter: Int = _
  implicit val iv: String = "Implicit Value"

  def methodA(pre: String, suf: String): Unit = {
    counter += 1
    println(s"$pre$name$suf")
  }

  def methodB(pre: String, suf: String): String = {
    counter += 1
    s"$pre$name$suf"
  }

  override def toString(): String =
    s"Member name=$name, counter=$counter, iv=$iv"
}