reflection

シンボル、構文木、型

EXPERIMENTAL

シンボル

シンボル (symbol) は名前 (name) とその名前が参照するクラスやメソッドのような実体 (entity) の間のバインディングを作るのに用いられる。Scala において定義され名前を付けられるものは全て関連付けられたシンボルを持つ。

シンボルは実体 (classobjecttrait など) もしくはメンバ (valvardef など) の宣言に関する全ての情報を格納するため、実行時リフレクションとコンパイル時リフレクション (マクロ) の両方において中心的な役割を果たす抽象体だ。

全てのシンボルにある基本的な name メソッドをはじめ、より複雑で込み入った概念である ClassSymbol に定義される baseClasses を取得するメソッドなど、シンボルは幅広い情報を提供する。 もう一つの一般的なシンボルの利用方法としてはメンバのシグネチャのインスペクトや、 クラスの型パラメータの取得、メソッドのパラメータ型の取得、フィールドの型の取得などが挙げられる。

シンボルのオーナーの階層

シンボルは階層化されている。 例えば、メソッドのパラメータを表すシンボルはそのメソッドのシンボルに所有されており、 メソッドのシンボルはそれを内包するクラス、トレイト、もしくはオブジェクトに所有されており、 クラスはそれを含むパッケージに所有されいてるという具合だ。

例えばトップレベルのパッケージのようなトップレベルの実体であるためにシンボルにオーナーが無い場合は、 NoSymbol という特殊なシングルトン・オブジェクトのオーナーが用いられる。 シンボルが無いことを表す NoSymbol は空を表わしたり、デフォルトの値として API の中で多用されている。 NoSymbolowner にアクセスすると例外が発生する。 Symbol 型によって提供される一般インターフェイスに関しては API doc を参照してほしい。

型シンボル (TypeSymbol)

型シンボル (TypeSymbol) は型、クラス、トレイトの宣言そして、型パラメータを表す。 より特定の ClassSymbol には当てはまらないメンバとして isAbstractTypeisContravariantisCovariant といったメソッドを持つ。

  • ClassSymbol: クラスやトレイトの宣言に格納される全ての情報へのアクセスを提供する。具体的には、name、修飾子 (isFinalisPrivateisProtectedisAbstractClass など)、 baseClassestypeParams など。

項シンボル (TermSymbol)

項シンボル (TermSymbol) は valvardef、そしてオブジェクトの宣言、パッケージや値のパラメータを表す。

  • メソッド・シンボル (MethodSymbol) は def の宣言を表す (TermSymbol のサブクラスだ)。メソッドが(基本)コンストラクタであるか、可変長引数をサポートするかなどの問い合わせを行うことができる。
  • モジュール・シンボル (ModuleSymbol) はオブジェクトの宣言を表す。moduleClass メンバを用いてオブジェクトに暗黙的に関連付けられているクラスを照会することができる。逆の照会も可能だ。モジュール・クラスから selfType.termSymbol によって関連付けられるモジュール・シンボルを得られる。

シンボル変換

状況によっては汎用の Symbol 型を返すメソッドを使う場面があるかもしれない。 その場合、汎用の Symbol 型をより特定の特殊化されたシンボル型へと変換することができる。 例えば MethodSymbol のインターフェイスを使いたいといった状況に合わせて、asMethodasClass のようなシンボル変換を用いると特殊化した Symbol のサブタイプに変換することができる。

具体例を用いて説明しよう。

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

scala> class C[T] { def test[U](x: T)(y: U): Int = ??? }
defined class C

scala> val testMember = typeOf[C[Int]].member(TermName("test"))
testMember: scala.reflect.runtime.universe.Symbol = method test

この場合、member は期待される MethodSymbol ではなく Symbol のインスタンスを返す。 このため、asMethod を使って MethodSymbol が返されたことを保証する必要がある。

scala> testMember.asMethod
res0: scala.reflect.runtime.universe.MethodSymbol = method test

自由シンボル

2つのシンボル型 FreeTermSymbolFreeTypeSymbol は入手可能な情報が不完全であるという特殊なステータスを持つシンボルだ。 これらのシンボルはレイフィケーションの過程において生成される (詳しくは構文木のレイフィケーションの節を参照)。 レイフィケーションがシンボルを特定できない場合 (例えば、ローカルクラスを参照しているため、あるシンボルが対応するクラスファイルから見つけることができない場合) 元の名前とオーナー、そして元の型シグネチャに似た代理シグネチャを持った合成のダミーシンボルへとレイファイする。このシンボルは自由型 (free type) と呼ばれる。 あるシンボルが自由型かどうかは sym.isFreeType を呼ぶことで確かめることができる。 また、tree.freeTypes を呼ぶことで特定の構文木とその部分木から参照されている全ての自由型のリストを取得することができる。 最後に、-Xlog-free-types を用いることでレイフィケーションが自由型を生成したときに警告を得ることができる。

名前が示すとおり、Type のインスタンスは対応するシンボルの型情報を表す。 これは、直接宣言もしくは継承されたメンバ (メソッド、フィールド、型エイリアス、抽象型、内部クラス、トレイトなど)、 基底型、型消去などを含む。他にも、型は型の適合性 (conformance) や等価性 (equivalence) を検査することができる。

型のインスタンス化

一般的には以下の 3通りの方法で Type を得ることができる。

  1. Universe にミックスインされている scala.reflect.api.TypeTagstypeOf メソッド経由。(最も簡単で、一般的な方法)
  2. IntBooleanAny、や Unit のような標準型はユニバースからアクセス可能だ。
  3. scala.reflect.api.TypestypeRefpolyType といったメソッドを使った手動のインスタンス化。(非推奨)

typeOf を用いた型のインスタンス化

多くの場合、型をインスタンス化するのには scala.reflect.api.TypeTags#typeOf メソッドを使うことができる。 これは型引数を受け取り、その引数を表す Type のインスタンスを返す。 具体例で説明すると、

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

scala> typeOf[List[Int]]
res0: scala.reflect.runtime.universe.Type = scala.List[Int]

この例では、型コンストラクタ List に型引数 Int が適用された scala.reflect.api.Types$TypeRef が返っている。

しかし、この方法はインスタンス化しようとしている型を手動で指定する必要があることに注意してほしい。 もし任意のインスタンスに対応する Type のインスタンスを取得しようとしてる場合はどうすればいいだろう? 型パラメータに context bound を付けたメソッドを定義すればいいだけだ。これは特殊な TypeTag を生成し、そこから任意のインスタンスに対する型を取得することができる:

scala> def getType[T: TypeTag](obj: T) = typeOf[T]
getType: [T](obj: T)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T])scala.reflect.runtime.universe.Type

scala> getType(List(1,2,3))
res1: scala.reflect.runtime.universe.Type = List[Int]

scala> class Animal; class Cat extends Animal
defined class Animal
defined class Cat

scala> val a = new Animal
a: Animal = Animal@21c17f5a

scala> getType(a)
res2: scala.reflect.runtime.universe.Type = Animal

scala> val c = new Cat
c: Cat = Cat@2302d72d

scala> getType(c)
res3: scala.reflect.runtime.universe.Type = Cat

注意: typeOf メソッドは、型パラメータを受け取る型 (例えば、A が型パラメータであるとき typeOf[List[A]]) では動作しない。 その場合は、代わりに scala.reflect.api.TypeTags#weakTypeOf を使うことができる。 これに関する詳細はこのガイドの TypeTags に関する節を参照。

標準型

IntBooleanAny、や Unit のような標準型はユニバースの definitions メンバからアクセス可能だ。 具体的には、

scala> import scala.reflect.runtime.universe
import scala.reflect.runtime.universe

scala> val intTpe = universe.definitions.IntTpe
intTpe: scala.reflect.runtime.universe.Type = Int

標準型のリストは scala.reflect.api.StandardDefinitions 内の StandardTypes トレイトにて定義されている。

型の一般的な演算

型の典型的な用例としては型の適合性の検査や、メンバの問い合わせがある。 型に対する演算を 3つに大別すると以下のようになる:

  1. 2つの型の間のサブタイプ関係の検査。
  2. 2つの型の間の等価性の検査。
  3. 渡された型の特定のメンバや内部型の問い合わせ。

サブタイプ関係

2つの Type インスタンスがあるとき、<:< を用いて簡単に一方がもう片方のサブタイプであるかを調べることができる。 (後で説明する例外的な場合においては、weak_<:< を使う)

scala> import scala.reflect.runtime.universe._
import scala-lang.reflect.runtime.universe._

scala> class A; class B extends A
defined class A
defined class B

scala> typeOf[A] <:< typeOf[B]
res0: Boolean = false

scala> typeOf[B] <:< typeOf[A]
res1: Boolean = true

weak_<:< メソッドは、2つの型の間の弱い適合性 (weak conformance) をチェックするのに使われることに注意。 これは典型的には数値型を取り扱う際に重要となる。

Scala の数値型は以下の順序付けに従っている (Scala 言語仕様 3.5.3 節):

Scala はいくつかの状況では、より一般的な適合性関係を用います。 もし S <: T であるか、あるいは、ST 両方がプリミティブな数値型で、次の順序中で ST の前にあるなら、型 S は型 T に弱く適合するといい、S <:w T と書きます。

| 弱適合性関係 | ————————— | Byte <:w Short | | Short <:w Int | | Char <:w Int | | Int <:w Long | | Long <:w Float | | Float <:w Double |

例えば、以下の if-式の型は弱い適合性によって決定されている。

scala> if (true) 1 else 1d
res2: Double = 1.0

上記の if-式では結果の型は 2つの型の弱い最小の上限境界 (weak least upper bound、つまり弱い適合性上で最小の上限境界) だと定義されている。

IntDouble の間では (上記の仕様により) Double が弱い適合性上での最小の上限境界だと定義されいるため、 Double が例の if-式の型だと推論される。

weak_<:< メソッドは弱い適合性をチェックすることに注意してほしい。 (それに対して、<:< は仕様 3.5.3 節の弱い適合性を考慮しない適合性を検査する) そのため、数値型 IntDouble の適合性関係を正しくインスペクトできる:

scala> typeOf[Int] weak_<:< typeOf[Double]
res3: Boolean = true

scala> typeOf[Double] weak_<:< typeOf[Int]
res4: Boolean = false

<:< を使った場合は IntDouble は互いに不適合であると間違った結果となる:

scala> typeOf[Int] <:< typeOf[Double]
res5: Boolean = false

scala> typeOf[Double] <:< typeOf[Int]
res6: Boolean = false

型の等価性

型の適合性同様に 2つの型の等価性を簡単に検査することができる。 2つの任意の型が与えられたとき、=:= メソッドを使うことでそれらが全く同一のコンパイル時型を表記しているかを調べることができる。

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

scala> def getType[T: TypeTag](obj: T) = typeOf[T]
getType: [T](obj: T)(implicit evidence$1: scala.reflect.runtime.universe.TypeTag[T])scala.reflect.runtime.universe.Type

scala> class A
defined class A

scala> val a1 = new A; val a2 = new A
a1: A = A@cddb2e7
a2: A = A@2f0c624a

scala> getType(a1) =:= getType(a2)
res0: Boolean = true

両方のインスタンスの型情報が寸分違わず一致している必要があることに注意してほしい。 例えば、以下のコードにおいて異なる型引数を取る 2つの List のインスタンスがある。

scala> getType(List(1,2,3)) =:= getType(List(1.0, 2.0, 3.0))
res1: Boolean = false

scala> getType(List(1,2,3)) =:= getType(List(9,8,7))
res2: Boolean = true

また、型の等価性を検査するためには常に =:= を使う必要があることに注意してほしい。 つまり、型エイリアスをチェックすることができない == は絶対に使ってはいけないということだ:

scala> type Histogram = List[Int]
defined type alias Histogram

scala> typeOf[Histogram] =:= getType(List(4,5,6))
res3: Boolean = true

scala> typeOf[Histogram] == getType(List(4,5,6))
res4: Boolean = false

見てのとおり、==HistogramList[Int] が異なる型であると間違った結果を出している。

型に対するメンバと宣言の照会

ある Type があるとき、特定のメンバや宣言を照会 (query) することができる。 Typeメンバ (member) には全てのフィールド、メソッド、型エイリアス、抽象型、内部クラス/オブジェクト/トレイトなどが含まれる。 Type宣言 (declaration) にはその Type が表すクラス/オブジェクト/トレイト内で宣言された (継承されなかった) メンバのみが含まれる。

ある特定のメンバや宣言の Symbol を取得するにはその型に関連する定義のリストを提供する membersdeclarations メソッドを使うだけでいい。単一のシンボルのみを返す meberdeclaration というメソッドもある。以下に 4つのメソッド全てのシグネチャを示す:

/** The member with given name, either directly declared or inherited, an
  * OverloadedSymbol if several exist, NoSymbol if none exist. */
def member(name: Universe.Name): Universe.Symbol

/** The defined or declared members with name name in this type; an
  * OverloadedSymbol if several exist, NoSymbol if none exist. */
def declaration(name: Universe.Name): Universe.Symbol

/** A Scope containing all members of this type
  * (directly declared or inherited). */
def members: Universe.MemberScope // MemberScope is a type of
                                  // Traversable, use higher-order
                                  // functions such as map,
                                  // filter, foreach to query!

/** A Scope containing the members declared directly on this type. */
def declarations: Universe.MemberScope // MemberScope is a type of
                                       // Traversable, use higher-order
                                       // functions such as map,
                                       // filter, foreach to query!

例えば、Listmap メソッドを照会するには以下のようにする。

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

scala> typeOf[List[_]].member("map": TermName)
res0: scala.reflect.runtime.universe.Symbol = method map

メソッドを照会するために member メソッドに TermName を渡していることに注意してほしい。 ここで、List の自分型である Self のような型メンバを照会する場合は TypeName を渡す:

scala> typeOf[List[_]].member("Self": TypeName)
res1: scala.reflect.runtime.universe.Symbol = type Self

型の全てのメンバや宣言を面白い方法で照会することもできる。 members メソッドを使って、渡された型の全ての継承もしくは宣言されたメンバを表す SymbolTraversable を取得することができる (MemberScopeApiTraversable を継承する)。 これにより、foreachfiltermap などの馴染み深いコレクションに対する高階関数を使って型のメンバを探検することができる。 例えば、List のメンバのうち private なものだけを表示したいとする:

scala> typeOf[List[Int]].members.filter(_.isPrivate).foreach(println _)
method super$sameElements
method occCounts
class CombinationsItr
class PermutationsItr
method sequential
method iterateUntilEmpty

構文木

構文木 (Tree) は、プログラムを表す Scala の抽象構文の基盤となっている。 これらは抽象構文木 (abstract syntax tree) とも呼ばれ、一般に AST と略される。

Scala リフレクションで、構文木を生成または利用する API には以下のようなものがある:

  1. Scala アノテーションは引数に構文木を用い、Annotation.scalaArgs として公開されている。(詳細はこのガイドのアノテーションの節を参照)
  2. 任意の式を受け取りその AST を返す reify という特殊なメソッド。
  3. マクロを用いたコンパイル時リフレクション (詳細はマクロ参照) とツールボックスを用いた実行時リフレクションは両方とも構文木を用いてプログラムを表現する。

ここで注意してほしいのは構文木は pos (Position)、 symbol (Symbol)、 と型検査の際に代入される tpe (Type) という 3つのフィールドの他は不変 (immutable) であることだ。

構文木の種類

構文木は以下の 3つのカテゴリーに大別することができる:

  1. TermTree のサブクラスは項を表す。例えば、メソッドの呼び出しは Apply ノードで表され、オブジェクトのインスタンス化は New ノードで行われる。
  2. TypTree のサブクラスはプログラムのソースコード中に現れる型を表す。例えば、List[Int]AppliedTypeTree へとパースされる。注意: TypTree は綴り間違いではないし、概念的に TypeTree とは異なるものだ。(例えば型推論などによって) コンパイラが Type を構築する場合にプログラムの AST に統合できるように TypeTree にラッピングされる。
  3. SymTree のサブクラスは定義を導入または参照する。新しい定義の導入の具体例としてはクラスやトレイトの定義を表す ClassDef や、フィールドやパラメータ定義を表す ValDef が挙げられる。既存の定義に対する参照の例としてはローカル変数やメソッドなど現行のスコープ内にある既存の定義を参照する Ident を挙げることができる。

上記のカテゴリー以外の構文木を目にすることがあるとすれば、それは典型的には合成的なものか短命な構築物だ。 例えば、各マッチケースをラッピングする CaseDef は項でも型でもなく、シンボルも持たない。

構文木をインスペクトする

Scala リフレクションは、ユニバース経由で構文木を視覚化する方法をいくつか提供する。渡された構文木があるとき、

  • show もしくは toString メソッドを使って構文木が表す擬似 Scala コードを表示することができる。
  • showRaw メソッドを使ってタイプチェッカが用いる生の構文木の内部構造を見ることができる。

具体例を使って説明しよう:

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

scala> val tree = Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2)

show メソッド (もしくは同等の toString) を使ってこの構文木が何を表しているかを見てみよう。

scala> show(tree)
res0: String = x.$plus(2)

見てのとおり、tree2 を項 x に加算する。

逆の方向に行くこともできる。ある Scala の式が与えられたとき、そこから構文木を取得した後で showRaw メソッドを用いてコンパイラやタイプチェッカが使っている生の構文木の内部構造を見ることができる。 例えば、以下の式があるとする:

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

scala> val expr = reify { class Flower { def name = "Rose" } }
expr: scala.reflect.runtime.universe.Expr[Unit] = ...

ここで、reify は Scala 式を受け取り TreeTypeTag をラッピングする Expr を返す。 (Expr の詳細に関してはこのガイドのの節を参照) expr が保持する構文木は以下のように取得できる:

scala> val tree = expr.tree
tree: scala.reflect.runtime.universe.Tree =
{
  class Flower extends AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    def name = "Rose"
  };
  ()
}

生の構文木の内部構造をインスペクトするには以下のように行う:

scala> showRaw(tree)
res1: String = Block(List(ClassDef(Modifiers(), TypeName("Flower"), List(), Template(List(Ident(TypeName("AnyRef"))), emptyValDef, List(DefDef(Modifiers(), nme.CONSTRUCTOR, List(), List(List()), TypeTree(), Block(List(Apply(Select(Super(This(tpnme.EMPTY), tpnme.EMPTY), nme.CONSTRUCTOR), List())), Literal(Constant(())))), DefDef(Modifiers(), TermName("name"), List(), List(), TypeTree(), Literal(Constant("Rose"))))))), Literal(Constant(())))

構文木の走査

構文木の内部構造が分かった所で、よくある次のステップは情報を抽出することだ。 これは構文木を走査することで行われ、以下の 2通りの方法がある:

  • パターンマッチングを用いた走査
  • Traverser のサブクラスを用いた走査

パターンマッチングを用いた走査

パターンマッチングを用いた走査は最も簡単で一般的な構文木の走査方法だ。 典型的にはある構文木の単一のノードの状態を知りたい場合にパターンマッチングを用いた走査を行う。 例えば、以下の構文木に1つだけある Apply ノードから関数と引数を取得したいとする。

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

scala> val tree = Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2)

tree に対してマッチをかけてやるだけでよく、Apply ケースの場合には Apply の関数と引数を返す:

scala> val (fun, arg) = tree match {
     |     case Apply(fn, a :: Nil) => (fn, a)
     | }
fun: scala.reflect.runtime.universe.Tree = x.$plus
arg: scala.reflect.runtime.universe.Tree = 2

パターンマッチを左辺項に移すことで上記と同じことをより簡潔に実現できる:

scala> val Apply(fun, arg :: Nil) = tree
fun: scala.reflect.runtime.universe.Tree = x.$plus
arg: scala.reflect.runtime.universe.Tree = 2

ノードは他のノード内に任意の深さで入れ子になることができるため、Tree は普通かなり複雑となることに注意してほしい。これを示す簡単な例として、上記の構文木に 2つ目の Apply を加えて既にある和に 3 を加算する:

scala> val tree = Apply(Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus")), List(Literal(Constant(3))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2).$plus(3)

これに上記と同じパターンマッチを適用すると外側の Apply ノードが得られ、それは上で見た x.$plus(2) を表す構文木を関数部分として格納する:

scala> val Apply(fun, arg :: Nil) = tree
fun: scala.reflect.runtime.universe.Tree = x.$plus(2).$plus
arg: scala.reflect.runtime.universe.Tree = 3

scala> showRaw(fun)
res3: String = Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus"))

特定のノードで止まることなく構文木全体を走査したり、特定の型のノードを収集してインスペクトするなどより複雑なタスクを行うためには Traverser を用いた走査の方が適しているかもしれない。

Traverser のサブクラスを用いた走査

初めから終わりまで構文木全体を走査する必要がある場合は、パターンマッチ中に現れうる全ての型に対する処理をする必要があるためパターンマッチングを用いた走査は適さない。 そのため、そのような場合は Traverser クラスを用いる。

Traevrser は幅優先探索を用いて渡された構文木の全てのノードを訪れることを保証する。

Traverser を使うには、Traverser を継承して traverse メソッドをオーバーライドする。 こうすることで必要なケースだけを処理するカスタムロジックを提供する。 例えば x.$plus(2).$plus(3) の構文木があるとき、全ての Apply ノードを収集したいとする:

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

scala> val tree = Apply(Select(Apply(Select(Ident(TermName("x")), TermName("$plus")), List(Literal(Constant(2)))), TermName("$plus")), List(Literal(Constant(3))))
tree: scala.reflect.runtime.universe.Apply = x.$plus(2).$plus(3)

scala> object traverser extends Traverser {
     |   var applies = List[Apply]()
     |   override def traverse(tree: Tree): Unit = tree match {
     |     case app @ Apply(fun, args) =>
     |       applies = app :: applies
     |       super.traverse(fun)
     |       super.traverseTrees(args)
     |     case _ => super.traverse(tree)
     |   }
     | }
defined module traverser

上のコードは渡された構文木のうち Apply ノードだけを探してリストを構築している。

これはスーパークラス Traverser で既に幅優先探索として実装されている traverse メソッドをサブクラス traverser のオーバーライドされた traverse メソッドが特別なケースを追加するという形で実現されている。 この特別なケースは Apply(fun, args) というパターンにマッチするノードのみに効用がある。 (funTree で表される関数、argsTree のリストで表される引数のリストとなる)

ある構文木がこのパターンにマッチすると (つまり、Apply ノードがあるとき)、 List[Apply] である applies に追加して、走査を続行する。

マッチした場合の処理で Apply にラッピングされた関数 fun に対して super.traverse そして引数のリスト args に対しては super.traverseTrees (super.traverse とほぼ同じものだが、単一の Tree の代わりに List[Tree] を受け取る) を呼び出していることに注意してほしい。 両方の呼び出しとも目的は簡単で、fun の中にも Apply パターンがあるか分からないため部分木に対してもデフォルトの Traversertraverse メソッドが確かに呼ばれるようにしている。 スーパークラスである Traverser は全ての入れ子になっている部分木に対して this.traverse を呼び出すため、Apply パターンを含む部分木もカスタムの traverse メソッドを呼び出すことが保証されている。

traverse を開始して、その結果の ApplyList を表示するには以下のように行う:

scala> traverser.traverse(tree)

scala> traverser.applies
res0: List[scala.reflect.runtime.universe.Apply] = List(x.$plus(2), x.$plus(2).$plus(3))

構文木の構築

実行時リフレクションを行う際に、構文木を手動で構築する必要は無い。 しかし、ツールボックスを用いて実行時コンパイルする場合やマクロを用いてコンパイル時リフレクションを行う場合はプログラムを表現する媒体として構文木が使われる。 そのような場合、構文木を構築する 3通りの方法がある:

  1. reify メソッドを用いる (可能な限りこれを使うことを推奨する)
  2. ツールボックスの parse メソッドを用いる
  3. 手動で構築する (非推奨)

reify を用いた構文木の構築

reify メソッドは Scala 式を引数として受け取り、その引数を Tree として表現したものを結果として返す。

Scala リフレクションでは、reify メソッドを用いた構文木の構築が推奨される方法だ。その理由を具体例を用いて説明しよう:

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

scala> { val tree = reify(println(2)).tree; showRaw(tree) }
res0: String = Apply(Select(Select(This(TypeName("scala")), TermName("Predef")), TermName("println")), List(Literal(Constant(2))))

ここで、単に println(2) という呼び出しを reify している。 つまり、println(2) という式をそれに対応する構文木の表現に変換している。そして、生の構文木の内部構造を出力している。 println メソッドが scala.Predef.println に変換されたことに注目してほしい。 このような変換は reify の結果がどこで用いられても意味が変わらないことを保証する。 例えば、この println(2) というコードが独自の println を定義するブロックに挿入されたとしてもこのコードの振る舞いには影響しない。

このような構文木の構築は、識別子のバインディングを保持するため健全 (hygenic) であるといわれる。

構文木のスプライシング

reify を使うことで複数の小さい構文木から 1つの構文木へと合成することもできる。これは Expr.splice (スプライス、「継ぎ足す」という意味) を用いて行われる。

注意: Exprreify の戻り値の型だ。型付けされた (typed) 構文木、TypeTag そして splice などのレイフィケーションに関連するいくつかのメソッドを含む簡単なラッパーだと思ってもらえばいい。 Expr に関する詳細はこのガイドの関連項目を参照。

例えば、splice を用いて println(2) を表す構文木を構築してみよう:

scala> val x = reify(2)
x: scala.reflect.runtime.universe.Expr[Int(2)] = Expr[Int(2)](2)

scala> reify(println(x.splice))
res1: scala.reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println(2))

ここで 2println をそれぞれ別に reify して、一方を他方の中に splice している。

しかし、reify の引数は妥当で型付け可能な Scala のコードであることが要求されることに注意してほしい。 println の引数の代わりに、println そのものを抽象化しようとした場合は失敗することを以下に示す:

scala> val fn = reify(println)
fn: scala.reflect.runtime.universe.Expr[Unit] = Expr[Unit](scala.this.Predef.println())

scala> reify(fn.splice(2))
<console>:12: error: Unit does not take parameters
            reify(fn.splice(2))
                            ^

見てのとおり、呼び出された関数の名前だけを捕捉したかったわけだが、コンパイラは引数無しの println という呼び出しをレイファイしたかったのだと決めてかかっている。

このようなユースケースは現在 reify を用いては表現することはできない。

ツールボックスの parse を用いた構文木の構築

ツールボックス (Toolbox) を使って構文木の型検査、コンパイル、および実行を行うことができる。 ツールボックスはまた、文字列を構文木へとパースすることができる。

注意: ツールボックスの仕様は scala-compiler.jar にクラスパスが通っていることを必要とする。

parse メソッドを使った場合に、前述の println の例がどうなるかみてみよう:

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

scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox

scala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
tb: scala.tools.reflect.ToolBox[scala.reflect.runtime.universe.type] = scala.tools.reflect.ToolBoxFactory$ToolBoxImpl@7bc979dd

scala> showRaw(tb.parse("println(2)"))
res2: String = Apply(Ident(TermName("println")), List(Literal(Constant(2))))

reify と違って、ツールボックスは型付けの要求を必要としないことに注目してほしい。 この柔軟性の引き換えに堅牢性が犠牲になっている。どういう事かと言うと、reify と違ってこの parseprintln が標準の println メソッドにバインドされていることが反映されていない。

注意: マクロを使っている場合は、ToolBox.parse を使うべきではない。マクロコンテキストに既に parse メソッドが組み込まれているからだ。具体例を使って説明しよう:

scala> import scala.language.experimental.macros
import scala.language.experimental.macros

scala> def impl(c: scala.reflect.macros.Context) = c.Expr[Unit](c.parse("println(2)"))
impl: (c: scala.reflect.macros.Context)c.Expr[Unit]

scala> def test = macro impl
test: Unit

scala> test
2
ツールボックスを用いた型検査

前に少し触れたが、ツールボックス (ToolBox) は文字列から構文木を構築する以外にも使い道があって、構文木の型検査、コンパイル、および実行を行うことができる。

プログラムの大まかな構造を保持する他に、構文木はプログラムの意味論に関する重要な情報を symbol (定義を導入または参照する構文木に割り当てられたシンボル) や tpe (構文木の型) という形で保持する。デフォルトでは、これらのフィールドは空だが、型検査をすることで充足される。

実行時リフレクションのフレームワークを利用する場合、型検査は ToolBox.typeCheck によって実装される。 コンパイル時にマクロを利用する場合は Context.typeCheck メソッドを使う。

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

scala> val tree = reify { "test".length }.tree
tree: scala.reflect.runtime.universe.Tree = "test".length()

scala> import scala.tools.reflect.ToolBox
import scala.tools.reflect.ToolBox

scala> val tb = runtimeMirror(getClass.getClassLoader).mkToolBox()
tb: scala.tools.reflect.ToolBox[scala.reflect.runtime.universe.type] = ...

scala> val ttree = tb.typeCheck(tree)
ttree: tb.u.Tree = "test".length()

scala> ttree.tpe
res5: tb.u.Type = Int

scala> ttree.symbol
res6: tb.u.Symbol = method length

上の例では、"test".length という呼び出しを表現する構文木を構築して、ToolBox tbtypeCheck メソッドを用いて構文木を型検査している。 見てのとおり、ttree は正しい型 Int を取得して、Symbol も正しく設定されている。

手動の構文木の構築

もし全てが失敗した場合は、手動で構文木を構築することもできる。これは最も低レベルな構文木を構築する方法で、他の方法がうまくいかなかった場合のみ挑むべき方法だ。 parse に比べてより柔軟な方法を提供するが、その柔軟性は過度の冗長さと脆弱さによって実現されている。

println(2) を使った例題を手動で構築すると、こうなる:

scala> Apply(Ident(TermName("println")), List(Literal(Constant(2))))
res0: scala.reflect.runtime.universe.Apply = println(2)

このテクニックの典型的なユースケースは単独では意味を成さない動的に構築された部分木を組み合わせて構文木を作る必要がある場合だ。 そのような場合、引数が型付けられていることを必要とする reify はおそらく不適切だろう。 構文木は個々の部分木では Scala ソースとして表現することができない式以下のレベルから組み立てられることがよくあるため、parse でもうまくいかないだろう。

blog comments powered by Disqus