こんにちは、SI部のfkmtです。

Scala本がジワジワと書店に増え始めてきたところに
Swiftの来襲によって、Scala本は一気に駆逐されてしまいましたが
めげずにScalaネタ書きます。

テーマはリフレクションです。

なお、この投稿は次のような方を読者として想定しています。

  • Scalaをそれなりに書ける
  • Javaのリフレクションをそれなりに書ける

また、掲載コードは次の環境で動作検証を行っています。

  • Scala 2.11.5
  • Java 1.8.0_20

はじめに:JavaとScalaの違い

Javaではリフレクションに、以下のようなクラスを使います。

  • java.lang.Class
  • java.lang.reflect.Method
  • java.lang.reflect.Field

リフレクション対象の各要素がそのままクラスになっていて、
クラスに関する情報の参照やクラスに対する操作はClassを使い、
メソッドをコントロールしたければMethodを、と素直でわかりやすいです。

それに対し、Scalaでは以下のようなクラスを使います。

  • scala.reflect.runtime.universe.Type
  • scala.reflect.runtime.universe.Symbol
  • scala.reflect.runtime.universe.Mirror

Typeは型情報の参照に、Mirrorは対象の操作に、それぞれ使います。
SymbolはTypeやMirrorと相互変換できたりする、リフレクション関連クラスを統合するモノです。

scala-reflection

Symbolは抽象化されていて、それぞれの要素に特化したMethodSymbolやClassSymbolなどがあります。
同様に、TypeやMirrorにも各要素に特化した型があります。

Javaがリフレクション要素ごとにクラス化しているのに対し、
Scalaでは要素に対する作用(参照や操作)をクラス化しているイメージです。

・・・日本語で書いても、わけがわからないよ。
手を動かして確認してみましょう。

使ってみる:RuntimeMirror編

Scalaコンソールを起動し、とりあえずいろいろやってみます。

準備

まず、何はなくともruntime.universeを全部importします。

import scala.reflect.runtime.universe._
// => import scala.reflect.runtime.universe._

ClassLoaderのMirrorをGet

最初に、大本になるClassLoaderのMirrorを取得します。

val clsLdrMirr = runtimeMirror(getClass.getClassLoader)
// => clsLdrMirr: reflect.runtime.universe.Mirror = JavaMirror with ...

※ runtimeMirrorはuniverseに定義されています。

クラスローダを引数にMirrorをGetします。
・・・が、実はscala.reflect.runtime.currentMirrorとして定義済みです。

clsLdrMirr == scala.reflect.runtime.currentMirror
// => res0: Boolean = true

クラスローダを引数に渡していることから想像できるとおり、
クラスローダにロードされたいろいろなクラスの情報が、このMirrorから取れます。

とりあえずStringで

StringのMirrorを取得してみます。
先に取得したClassloaderMirrorのreflectメソッドにStringインスタンスを渡せば取れます。

val strMirr = clsLdrMirr.reflect("moji")
// => strMirr: reflect.runtime.universe.InstanceMirror = instance mirror for moji

InstanceMirrorなるモノが取れました。

Mirror <-> Symbol <-> Type は相互に変換が可能です。
これで、型の情報を調べたいときはTypeに、メソッド呼び出しなどの操作を行いたいときはMirrorに・・・
というように使います。

変換してみる

Symbolへの変換・・・というか取得は、symbolというフィールドからできます。

val strSym = strMirr.symbol
// => strSym: reflect.runtime.universe.ClassSymbol = class String

SymbolからTypeを取るにはtypeSignatureです。

val strTyp = strSym.typeSignature
// => strTyp: reflect.runtime.universe.Type = ...

省略しましたが、コンソールにはズラズラとStringのメンバが表示されます。
Typeからはいろいろと参照できそうなことがヒシヒシと伝わってきます。

lengthを呼び出してみる

何かメソッドを呼び出してみます。
被験者は「length」さんです。

手順は以下のようになります。

  • lengthのSymbolをTypeから取得
  • String(”moji”)のMirrorにlengthのSymbolを渡してlengthのMethodMirrorを取得
  • lengthのMethodMirrorを実行

Typeのmemberにメソッドの名前を渡すと、Symbolが返ってきます。

※ フィールドの場合はフィールド名を渡します。

val lenSym = strTyp.member(TermName("length"))
// => lenSym: reflect.runtime.universe.Symbol = method length

文字列で渡したいところですが、どういうわけか引数はTermNameです。

※ TermNameはuniverseに定義されています。


存在しないメソッド(やフィールド)を渡すと・・・

val nikoniko = strTyp.member(TermName("(^o^)"))
// => nikoniko: reflect.runtime.universe.Symbol = <none>

nikoniko == NoSymbol
// => res1: Boolean = true

NoSymbolなるモノが返るようになっています。


さて、lengthにハナシを戻して。

上で、型情報はType、操作はMirror、と書きました。
取得したlengthを実行するにはMirrorを取得する必要があります。

lengthの実行可能なMirrorを取得するには、StringのInstanceMirrorに、
今取得したlengthのSymbolを渡してあげます。

val lenMirr = strMirr.reflectMethod(lenSym.asMethod)
// => lenMirr: reflect.runtime.universe.MethodMirror = method mirror for def length(): Int (bound to moji)

「bound to moji」と表示されてますね。
lengthはstaticではないので、実行対象となるインスタンスと紐づく必要があります。
strMirrは”moji”のインタンスから取得したMirrorなので、”moji”にバインドされるわけです。

実行してみます。
“moji”は4文字なので「4」が取得できるはずです。

lenMirr()
// => res2: Any = 4

できました!

使ってみる:TypeTag編

先の例では、RuntimeMirrorからのリフレクションを試しましたが
TypeTagからのリフレクションの方法もあります。

今度はTypeTagから辿ってString#toUpperCaseを実行してみます。

まずは、import。

import scala.reflect.runtime.universe._
// => import scala.reflect.runtime.universe._

そして、Typeを取得します。

val strTyp = typeOf[String]
// => strTyp: reflect.runtime.universe.Type = String

typeOfに型を渡すだけです。


インスタンスからTypeを取る方法

typeOfは、TypeTags.scalaに以下のように定義されています。

def typeOf[T](implicit ttag: TypeTag[T]): Type = ttag.tpe

同じ要領で、インスタンスからでもTypeを取得することもできます。

def getType[T: TypeTag](x: T) = typeTag[T].tpe
// => getType: [T](x: T)(implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.Type</code>

getType(123) // インスタンスから
// => res0: reflect.runtime.universe.Type = Int

typeOf[Int] // 型から
// => res1: reflect.runtime.universe.Type = Int

リフレクションAPIに標準で用意してくれればいいのに・・・。


さて、ハナシを戻して。

toUpperCaseを取得します。

val uppCase = strTyp.member(TermName("toUpperCase"))
// => uppCase: reflect.runtime.universe.Symbol = value toUpperCase

しかし、ここで問題が。

lengthと違い、toUpperCaseには「引数のないもの」「Localeを引数に取るもの」の2つがあり、
どちらかを選択しないといけません。

今回は「引数のないもの」を選択することにします。

オーバーロードされている場合、alternativesを参照します。

uppCase.alternatives
// => List[reflect.runtime.universe.Symbol] = List(method toUpperCase, method toUpperCase)

Symbolのリストが返ってきて、toUpperCaseが2つあることが分かります。

それぞれのSymbolのパラメータについて調べるには、paramListsを参照します。

uppCase.alternatives(0).asMethod.paramLists
// => res1: List[List[reflect.runtime.universe.Symbol]] = List(List(value x$1))

uppCase.alternatives(1).asMethod.paramLists
// => res2: List[List[reflect.runtime.universe.Symbol]] = List(List())

※ paramListsはMethodSymbolに定義されているので、適宜asMethodでMethodSymbolにします。

paramListsの型はList[List[Symbol]]です。


List[List[Symbol]]だと? どうして二重になってるんだ!?

・・・と思いますよね。

これはScalaでは以下のように引数リストなしのdefが定義できるためです。

class Sample {
  def meth0 = "!"
  def meth1() = "!!"
  def meth2(x: String) = s"$x!!!"
}

このSampleクラスのmethたちをリフレクションしてみると、以下のようになります。

typeOf[Sample].member(TermName("meth0")).asMethod.paramLists
// => res0: List[List[reflect.runtime.universe.Symbol]] = List()

typeOf[Sample].member(TermName("meth1")).asMethod.paramLists
// => res1: List[List[reflect.runtime.universe.Symbol]] = List(List())

typeOf[Sample].member(TermName("meth2")).asMethod.paramLists
// => res2: List[List[reflect.runtime.universe.Symbol]] = List(List(value x))

おわかりいただけただろうか。


・・・再びハナシを戻して。

paramListsの中身はSymbolなので、引数の1つ1つについて調べようと思えばいろいろ調べられます。
今回は、引数なしの方を選択すればよいので、paramListsのサイズで調べることにします。

val uppCaseNoArg = uppCase.alternatives.find(_.asMethod.paramLists.flatten.isEmpty)
// => uppCaseNoArg: Option[reflect.runtime.universe.Symbol] = Some(method toUpperCase)

※ paramLists.flattenで二重リストを展開し、size=0のものをfindします。

続いて、toUpperCaseを適用するStringインスタンスを用意します。

val mojiMirr = scala.reflect.runtime.currentMirror.reflect("moji")
// => mojiMirr: reflect.runtime.universe.InstanceMirror = instance mirror for moji

そして実行。

mojiMirr.reflectMethod(uppCaseNoArg.get.asMethod).apply()
// => res3: Any = MOJI

“MOJI”を取得できました!

その他のトピック

Javaにはできないこと その1

Scalaではジェネリクスで指定した型パラメータを参照できます。
List[Int]のIntを扱えるのです。

typeOf[List[Int]].typeArgs
// => res0: List[reflect.runtime.universe.Type] = List(Int)

Mapなど、型パラメータが2つの場合は・・・

typeOf[Map[String, Int]].typeArgs
// => res1: List[reflect.runtime.universe.Type] = List(String, Int)

キーと値の型の、2要素のリストで返ってきます。

Javaにはできないこと その2

引数の名前が取れます。

class Sample {
  def meth(foo: Int) = foo * 100
}

↑のようなクラスがある場合に、methメソッドの第1引数の名前「foo」が取れます。

typeOf[Sample].member(TermName("meth")).asMethod.paramLists.flatten.head.name
// => res1: reflect.runtime.universe.Symbol#NameType = foo

なお、Javaクラスのメソッド引数名は取得できません。

// java.util.Mapのputメソッドの引数名を取得してみる
typeOf[ java.util.Map[_, _] ]
  .member(TermName("put")).asMethod
  .paramLists.flatten
  .map(_.name).mkString(" -> ")
// => res0: String = x$1 -> x$2

// scala.collection.mutable.Mapのupdatedメソッドの引数名を取得してみる
typeOf[ scala.collection.mutable.Map[_, _] ]
  .member(TermName("updated")).asMethod
  .paramLists.flatten
  .map(_.name).mkString(" -> ")
// => res1: String = key -> value

ScalaのMap#updatedの引数2つについては、それぞれ「key」「value」と名称が取得できています。
JavaのMap#putの引数2つは、「x$1」「x$2」となっており、ソースコードで使用している名前は取得できません。

Typeの比較

Typeには「<:<」や「=:=」といったメソッドが定義されており、これでType同士を比較できます。

「<:<」継承関係にあるかどうか。

typeOf[String] <:< typeOf[Any]
// => res0: Boolean = true

typeOf[String] <:< typeOf[AnyVal]
// => res1: Boolean = false

typeOf[String] <:< typeOf[AnyRef]
// => res2: Boolean = true

typeOf[String] <:< typeOf[String] // これもtrue
// => res3: Boolean = true

typeOf[List[String]] <:< typeOf[List[AnyRef]] // Listは共変なので
// => res4: Boolean = true

typeOf[Array[String]] <:< typeOf[Array[AnyRef]] // Arrayは非変なので
// => res5: Boolean = false

「=:=」同じかどうか。厳密に比較するときに。

typeOf[Int] =:= typeOf[Int]
// => res6: Boolean = true

typeOf[Int] =:= typeOf[Boolean]
// => res7: Boolean = false

typeOf[Int] =:= typeOf[Short]
// => res8: Boolean = false

おわりに

Mirrorからのインスタンスを起点としたリフレクションと
TypeTagからの型を起点としたリフレクションについて書いてみましたが
Scalaのリフレクションの「雰囲気」は伝わりましたでしょうか。

Typeで調べ、Mirrorで操作する、という点さえ押さえておけば
あとはScaladocを見て、いろいろなことができると思います。

Javaのリフレクションよりも、ごちゃごちゃした印象はありますが
ScalaリフレクションはJavaにはできないこともできます。

しかし、そこまでの機能が必要ないならJavaのリフレクションも使えます。
必要に応じて使い分けるができることもScalaの利点の1つと思います。

(リフレクションを使うかどうかはともかく)
この投稿をきっかけに、Scalaに興味をもっていただけたら幸いです。