前回の続きです。今回はジェネリクスの利用まで、少し踏み込んでみます。
これまでに書いたエントリのまとめは、こちらです。
導入編
http://d.hatena.ne.jp/Kazuhira/20130730/1375192075
定義情報取得編 - 1
http://d.hatena.ne.jp/Kazuhira/20130801/1375370390
定義情報取得編 - 2
http://d.hatena.ne.jp/Kazuhira/20130803/1375526971
インスタンス操作編
http://d.hatena.ne.jp/Kazuhira/20130804/1375604912
オマケ
http://d.hatena.ne.jp/Kazuhira/20130804/1375607954
前回同様、scala-reflect.jarへの依存関係の準備と、以下のimport文は定義済みとします。
import scala.reflect.runtime.universe
なお、今回は、Typeの取得はメソッドを定義せずに
universe.typeOf[...]
みたいな表記でいこうと思います。
指定のクラスのみで定義されている、全メンバーを取得したい
Type#declarationsを使用します。
universe.typeOf[...].declarations
Type
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$Type
戻り値の型は、MemberScopeとなっていてコレクション的に扱えるので、適宜欲しい情報を絞り込んでください。
MemberScope
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Scopes$MemberScope
高階関数系の引数には、Symbolが渡ってきます。
Symbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$Symbol
使用例
取得対象の型。
class SuperClass { val superClasssValField: String = "a" var superClasssVarField: String = "b" private val superClassPrivateValField: String = "c" private var superClassPrivateVarField: String = "d" def methodSuper: String = "foo" } trait Trait { val traitValField: String = "z" def methodTrait: String = "bar" } class SubClass extends SuperClass with Trait { val valField: String = "A" var varField: String = "B" private val privateValField: String = "C" private var privateVarField: String = "D" def methodSub: String = "fuga" }
使用したコードのサンプル。
val theType = universe.typeOf[SubClass] val printSymbol = (symbol: universe.Symbol) => { if (symbol.asTerm.isVal) { val valTermSymbol = symbol.asTerm println(s"""|Val => | owner: ${valTermSymbol.owner} | encodedName: ${valTermSymbol.name.encoded} | decodedName: ${valTermSymbol.name.decoded}""".stripMargin) } else if (symbol.asTerm.isVar) { val varTermSymbol = symbol.asTerm println(s"""|Var => | owner: ${varTermSymbol.owner} | encodedName: ${varTermSymbol.name.encoded} | decodedName: ${varTermSymbol.name.decoded}""".stripMargin) } else if (symbol.isMethod) { val methodSymbol = symbol.asMethod println(s"""|Method => | owner: ${methodSymbol.owner} | encodedName: ${methodSymbol.name.encoded} | decodedName: ${methodSymbol.name.decoded} | isConstructor: ${methodSymbol.isConstructor} | isPrimaryConstructor: ${methodSymbol.isPrimaryConstructor} | paramss: ${methodSymbol.paramss} | returnType: ${methodSymbol.returnType}""".stripMargin) } } theType.declarations.foreach(printSymbol)
declarationsを使っているのは、最後ですが…。
このコードを実行すると、SubClassにのみ定義された情報が出力され、親クラスやトレイトに定義されたものは出力されません。
親クラスやトレイトに定義されたものを含めた、全メンバーを取得したい
Type#membersを使用します。取得可能な範囲が広がるだけで、それ以外の使い方はdeclarationsと同じです。
使用例
取得対象の型。
class SuperClass { val superClasssValField: String = "a" var superClasssVarField: String = "b" private val superClassPrivateValField: String = "c" private var superClassPrivateVarField: String = "d" def methodSuper: String = "foo" } trait Trait { val traitValField: String = "z" def methodTrait: String = "bar" } class SubClass extends SuperClass with Trait { val valField: String = "A" var varField: String = "B" private val privateValField: String = "C" private var privateVarField: String = "D" def methodSub: String = "fuga" }
使用したコードのサンプル。
val theType = universe.typeOf[SubClass] val printSymbol = (symbol: universe.Symbol) => { if (symbol.asTerm.isVal) { val valTermSymbol = symbol.asTerm println(s"""|Val => | owner: ${valTermSymbol.owner} | encodedName: ${valTermSymbol.name.encoded} | decodedName: ${valTermSymbol.name.decoded}""".stripMargin) } else if (symbol.asTerm.isVar) { val varTermSymbol = symbol.asTerm println(s"""|Var => | owner: ${varTermSymbol.owner} | encodedName: ${varTermSymbol.name.encoded} | decodedName: ${varTermSymbol.name.decoded}""".stripMargin) } else if (symbol.isMethod) { val methodSymbol = symbol.asMethod println(s"""|Method => | owner: ${methodSymbol.owner} | encodedName: ${methodSymbol.name.encoded} | decodedName: ${methodSymbol.name.decoded} | isConstructor: ${methodSymbol.isConstructor} | isPrimaryConstructor: ${methodSymbol.isPrimaryConstructor} | paramss: ${methodSymbol.paramss} | returnType: ${methodSymbol.returnType}""".stripMargin) } } theType.members.foreach(printSymbol)
今度は、親クラスやトレイトの定義も含めて、すべて出力されます。
前回詳しく触れませんでしたが、memberもしくはmembers、declarationもしくはdeclarationsの違いは、取得対象に上位のクラスが入るかどうかになります。
メソッドのパラメータや戻り値が知りたい
MethodSymbol#paramssと、MethodSymbol#returnTypeを使用します。
val method = universe.typeOf[...].member(universe.newTermName("メソッド名")).asMethod method.paramss method.returnType
パラメータがparamssで、戻り値がreturnTypeですね。
paramssはListのListのSymbolで、実体はTermSymbolっぽいです。returnTypeは文字通りTypeが返ります。
MethodSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$MethodSymbol
TermSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$TermSymbol
使用例
取得対象の型。
class SampleClass { def methodA(param: String): Int = 10 def methodB(): String = "Hello World" def methodC(param1: String, param2: Int): (String, String) = ("foo", "bar") }
使用したコードのサンプル。
val theType = universe.typeOf[SampleClass] val methodA = theType.member(universe.newTermName("methodA")).asMethod methodA.paramss match { case List(arg :: Nil) => require(arg.typeSignature =:= universe.typeOf[String]) require(methodA.returnType =:= universe.typeOf[Int]) } val methodB = theType.member(universe.newTermName("methodB")).asMethod methodB.paramss match { case List(Nil) => require(methodB.returnType =:= universe.typeOf[String]) } val methodC = theType.member(universe.newTermName("methodC")).asMethod methodC.paramss match { case List(arg1 :: arg2 :: Nil) => require(arg1.typeSignature =:= universe.typeOf[String]) require(arg2.typeSignature =:= universe.typeOf[Int]) require(methodC.returnType =:= universe.typeOf[Tuple2[String, String]]) require(methodC.returnType != universe.typeOf[Tuple2[Int, Int]]) }
一応、引数の数や型、戻り値の情報が取得できていることがわかります。Tuple2のようなジェネリックな型の場合は、適用されている型パラメータまでわかっているようです。
asTypeとかasMethodとか使って変換っぽいことしてますが、あれは何?
メンバーなどを取得した時は、だいたいSymbolという形になっているので、より詳細な情報が欲しい場合はTypeSymbol、ClassSymbol、MethodSymbolなどのより適切なビューに変換します。
キャストだと思えばいいと思います。
ただ、実際に変換できるかどうかはもちろん定義次第なので、
class SampleClass { def method: String = "hello" }
みたいな定義に対して、メソッドをTypeSymbolに変換しようとすると
val theType = universe.typeOf[SampleClass] try { theType.member(universe.newTermName("method")).asType } catch { case e: ScalaReflectionException => println(s"Got Exception: $e") }
見事にコケます。
Got Exception: scala.ScalaReflectionException: method method is not a type
こうなりたくなければ、事前にis[Symbol名]で変換可能か確認してください。
val theType = universe.typeOf[SampleClass] val symbol = theType.member(universe.newTermName("method")) if (symbol.isType) { println("Type?") } else if (symbol.isMethod) { println("Method?") }
この場合、実行結果は
Method?
となります。
is[Symbol名]、as[Symbol名]のメソッドがされているのは、Symbolになります。
Symbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$Symbol
注意点としては、TypeSymbolやMethodSymbolなど大抵のSymbolはTermSymbolになるので、条件分岐を書く時にはTermSymbolの扱いに気をつけないと全部飲み込まれちゃったりするので気をつけてくださいね。
memberとかdeclarationってTermNameしか引数に取りませんけど、オーバーロードしたメソッドはどう扱われるんですか?
MethodSymbolに変換しようとすると、例外で死にます。
例えば、以下のようなクラス定義があって
class SampleClass { def overloadMethod(param1: String): String = "1" def overloadMethod(param1: String, param2: String): String = "2" def overloadMethod(param1: String, param2: String, params: String*): String = "varargs" }
こんなコードを書いて実行すると
val theType = universe.typeOf[SampleClass] try { theType.member(universe.newTermName("overloadMethod")).asMethod } catch { case e: ScalaReflectionException => println(s"Got Exception: $e") }
コケます。
Got Exception: scala.ScalaReflectionException: value overloadMethod encapsulates multiple overloaded alternatives and cannot be treated as a method. Consider invoking `<offending symbol>.asTerm.alternatives` and manually picking the required method
よくよく見ると、「オーバーロードされているから、ちゃんと選んでね」と言ってます。
なので、filterやsuchThatメソッドで対象のメソッドを選出します。
val method = theType.member(universe.newTermName("overloadMethod")).filter { s => s.asMethod.paramss match { case List(arg1 :: arg2 :: arg3) if s.asMethod.isVarargs => true case _ => false } }.asMethod theType.member(universe.newTermName("overloadMethod")).suchThat { s => s.asMethod.paramss match { case List(arg1 :: arg2 :: arg3) if s.asMethod.isVarargs => true case _ => false } }.asMethod
ちゃんとひとつに絞り込めば、asMethodで変換可能になります。
ここでは、以下のメソッド定義を選びました。
def overloadMethod(param1: String, param2: String, params: String*): String = "varargs"
filter、suchThatは、Symbolに定義されています。
Symbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$Symbol
なお、filterとsuchThatの違いですが、filterは述語を適用した結果にまだ複数のメソッド(Symbol)が残っても大丈夫ですが、suchThatはひとつに絞れなかった場合はAssertionErrorがスローされます。
theType.member(universe.newTermName("overloadMethod")).filter { s => s.asMethod.paramss match { case List(arg :: Nil) => true case List(arg1 :: arg2 :: arg3) if s.asMethod.isVarargs => true case _ => false } } try { theType.member(universe.newTermName("overloadMethod")).suchThat { s => s.asMethod.paramss match { case List(arg :: Nil) => true case List(arg1 :: arg2 :: arg3) if s.asMethod.isVarargs => true case _ => false } } } catch { case e: AssertionError => println(s"Got Exception: $e") }
suchThatの方は絞り込めていないので、こうなります。
Got Exception: java.lang.AssertionError: assertion failed: List(method overloadMethod, method overloadMethod)
まあ、filterも適用時にまだひとつに絞れなくてもいいだけで、asMethodとかで変換する時には絞れてないともちろんダメなわけですが。
try { theType.member(universe.newTermName("overloadMethod")).filter { s => s.asMethod.paramss match { case List(arg :: Nil) => true case List(arg1 :: arg2 :: arg3) if s.asMethod.isVarargs => true case _ => false } }.asMethod } catch { case e: ScalaReflectionException => println(s"Got Exception: $e") }
つまり、最初の状態と同じですね。
Got Exception: scala.ScalaReflectionException: value overloadMethod encapsulates multiple overloaded alternatives and cannot be treated as a method. Consider invoking `<offending symbol>.asTerm.alternatives` and manually picking the required method
もうひとつの取得方法としては、例外のメッセージにも書かれているように、1度TermSymbolに変換してalternativesから取得したいSymbolを選んでください。
val overloadMethod = theType.member(universe.newTermName("overloadMethod")).asTerm.alternatives val method = overloadMethod.find { s => s.asMethod.paramss match { case List(arg1 :: arg2 :: arg3) if s.asMethod.isVarargs => true case _ => false } }.head.asMethod
TermSymbolへは、オーバーロードされたメソッドを解決できないままでも変換可能です。TermSymbol#alternativesには、オーバーロードされているSymbolが格納されているので、この中から選出してください。
ジェネリクスの情報って、取れるんですか?
取れます。TypeSymbol、ClassSymbol、MethodSymbolなどのtypeParamsメソッドで取得することができます。
val theType = universe.typeOf[...] // 型の型パラメータ theType.typeSymbol.asType.typeParams // メソッドの型パラメータ theType.member(universe.newTermName("method")).asMethod.typeParams
TypeSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$TypeSymbol
ClassSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$ClassSymbol
MethodSymbol
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$MethodSymbol
使用例
取得対象の型。
class ParameterizedClass[T](val field: T) { def method[A, B](param1: A, param2: B): T = field }
使用したコードのサンプル。
val theType = universe.typeOf[ParameterizedClass[String]] theType.typeSymbol.asType.typeParams match { case typeArg :: Nil => require(typeArg.typeSignature =:= universe.WildcardType) require(typeArg.toString == "type T") } val methodSymbol = theType.member(universe.newTermName("method")).asMethod methodSymbol.typeParams match { case t1 :: t2 :: Nil => require(t1.typeSignature =:= universe.WildcardType) require(t2.typeSignature =:= universe.WildcardType) require(t1.toString == "type A") require(t2.toString == "type B") require(methodSymbol.returnType =:= universe.WildcardType) require(methodSymbol.returnType.toString == "T") }
型パラメータを持ったものは、WildcardTypeとして扱われるようですね。
適用した型パラメータの値とか、わかりませんか?
TypeRefかType#asSeenFromを使えば、わかるみたいですよ。
TypeRef
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$TypeRef
Type
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$Type
これは、実際の使用例を見た方が早そうですね。
使用例
取得対象の型。
class ParameterizedClass[T](val field: T) { def method[A, B](param1: A, param2: B): T = field }
使用したコードのサンプル。
まずは、TypeRef。
universe.typeOf[ParameterizedClass[String]] match { case universe.TypeRef(prefix, symbol, typeArgs) => require(typeArgs == List(universe.typeOf[String])) }
TypeRefでパターンマッチをかけた時の3つ目の値に、型パラメータが入ります。ここでは、適用した型がStringであることがわかっていますね。
続いて、Type#asSeenFrom。
val theType = universe.typeOf[ParameterizedClass[String]] val classSymbol = theType.typeSymbol.asClass theType.typeSymbol.asType.typeParams match { case arg :: Nil => val typeParam = arg.asType.toType require(typeParam.asSeenFrom(theType, classSymbol) =:= universe.typeOf[String]) }
TypeSymbol#typeParamsで取得できるTypeSymbolをTypeに変換した後に、持ち主のTypeとClassSymbolを渡すことで適用した型がわかります。この部分は、TypeRefでパターンマッチをかけている時の実体に他ならないのですが。
ジェネリクスの上限とか下限とか、わかりませんか?
これはあまり自信がないですが、クラスの型パラメータはExistentialTypeとTypeBounds、メソッドの場合はTypeBoundsを使えばわかりそうです。
ExistentialType
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$ExistentialType
ExistentialTypeExtractor
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$ExistentialTypeExtractor
TypeBounds
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$TypeBounds
TypeBoundsExtractor
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$TypeBoundsExtractor
これも、使用例を見た方がよさそうですね。
使用例
取得対象の型。
class BoundParameterizedClass[A <: AnyVal](val field: A) { def methodA[T <: java.lang.Number](param: T): T = param def methodB[T >: java.util.ArrayList[_]](param: T): T = param }
使用したコードのサンプル。
まずは、クラスに適用された型から。
val theType = universe.typeOf[BoundParameterizedClass[Int]] theType.erasure match { case universe.ExistentialType(quantified, underlying) => quantified.foreach { s => require(s.typeSignature =:= universe.WildcardType) s.typeSignature match { case universe.TypeBounds(lo, hi) => require(lo =:= universe.definitions.NothingTpe) require(hi =:= universe.typeOf[AnyVal]) } } }
Type#erasureを一度呼ぶ必要がありそうな感じです。その後、ExistentialTypeで取得できるquantifiedから判定します。quantifiedはSymbolのListになっているので、各SymbolをTypeに変換した後、TypeBoundsで上限と下限が取得できます。
今回は、上限のみを指定しているので、下限はNothingですね。
続いて、メソッド。
val theType = universe.typeOf[BoundParameterizedClass[Int]] theType.member(universe.newTermName("methodA")).asMethod.typeParams.foreach { s => require(s.typeSignature =:= universe.WildcardType) s.typeSignature match { case universe.TypeBounds(lo, hi) => require(lo =:= universe.definitions.NothingTpe) require(hi =:= universe.typeOf[java.lang.Number]) } } theType.member(universe.newTermName("methodB")).asMethod.typeParams.foreach { s => require(s.typeSignature =:= universe.WildcardType) s.typeSignature match { case universe.TypeBounds(lo, hi) => require(lo =:= universe.typeOf[java.util.ArrayList[_]]) require(hi =:= universe.definitions.AnyTpe) } }
こちらは、asMethodで変換後、MethodSymbol#typeParamsで取得できるSymbolをTypeBoundsで分解してるだけですね。下限のみを指定した場合は、上限はAnyとして取得できるようです。
よく使うTypeとかClassとか、どこかに定義されてないんですか?
すでに少し使っていますが、DefinitionApiを使用します。
DefinitionsApi
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.StandardDefinitions$DefinitionsApi
これは、Universeのメンバーにdefinitionsとして定義してあります。どんなTypeやClassSymbolが定義されているかは、ドキュメントを参照してください。
使用例です。
require(universe.definitions.IntTpe =:= universe.typeOf[Int]) require(universe.definitions.ListClass == universe.typeOf[List[_]].typeSymbol.asClass)
親クラスに適用された型パラメータを知りたいんですが?
こういうクラス定義の時に
class ParameterizedClass[T](val field: T) { def method[A, B](param1: A, param2: B): T = field } class SubClass(field: String) extends ParameterizedClass[String](field)
以下の
extends ParameterizedClass[String]
Stringの部分を取得したいようなケース。
Type#asSeenFromに加えて、ThisTypeを使用します。
Type
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$Type
ThisType
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Types$ThisType
使用例です。
val theType = universe.typeOf[SubClass] val superClassSymbol = universe.typeOf[ParameterizedClass[_]].typeSymbol.asClass val typeParam = superClassSymbol.typeParams(0).asType.toType require(typeParam.asSeenFrom(universe.ThisType(theType.typeSymbol), superClassSymbol) =:= universe.typeOf[String])
型情報を文字列から取得したいんですが?
今まで、Typeを取るのに
universe.typeOf[A]
とか
universe.typeTag[A].tpe
を使ってきましたが、これだと使える型がコンパイル時にわかっているものだけしか使えなくなってしまいます。
JavaのClass.forNameのように、文字列からTypeを取得するには、JavaMirrorを使用します。
JavaMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.JavaMirrors$JavaMirror
以下に、JavaのStringとArrayListを取得する例を記載します。
val runtimeMirror = universe.runtimeMirror(Thread.currentThread.getContextClassLoader) val stringClassSymbol = runtimeMirror.staticClass("java.lang.String") require(stringClassSymbol.toType =:= universe.typeOf[String]) val arrayListClassSymbol = runtimeMirror.staticClass("java.util.ArrayList") require(arrayListClassSymbol.toType.erasure =:= universe.typeOf[java.util.ArrayList[_]])
JavaMirrorは、universe#runtimeMirrorから取得できます。引数のクラスローダーは、適宜。あとは、JavaMirror#staticClassでClassSymbolが取得できます。
あとは、toTypeすればTypeが得られるのですが、ArrayListみたいなジェネリックな型はType#erasureを挟まないと判定が難しい??
TypeやClassSymbolから、JavaのClassクラスに戻せないんですか?また、その逆は?
JavaMirrorを使用すれば、可能です。
JavaMirror
http://www.scala-lang.org/api/current/index.html#scala.reflect.api.JavaMirrors$JavaMirror
JavaMirror#runtimeClassメソッドでTypeもしくはClassSymbolを、JavaのClassクラスに変換できます。また、JavaMirror#classSymbolメソッドで、ClassクラスからClassSymbolを得ることができます。
以下、使用例です。
val theType = universe.typeOf[String] val runtimeMirror = universe.runtimeMirror(Thread.currentThread.getContextClassLoader) require(runtimeMirror.runtimeClass(theType) == classOf[String]) require(runtimeMirror.runtimeClass(theType.typeSymbol.asClass) == classOf[String]) require(runtimeMirror.classSymbol(classOf[String]) == universe.typeOf[String].typeSymbol.asClass)
とりあえず、定義情報の取得に関しては、こんなところではないでしょうか。最後は、インスタンスの操作を扱って終了したいと思います。