EXPERIMENTAL
アノテーション
Scala において宣言は scala.annotation.Annotation
のサブタイプを用いて注釈を付けることができる。
さらに、Scala は Java のアノテーションシステムに統合するため、標準 Java コンパイラによって生成されたアノテーションを取り扱うこともできる。
アノテーションは、それが永続化されていればリフレクションを使ってインスペクトすることができるため、アノテーション付きの宣言を含むクラスファイルから読み込むことができる。カスタムアノテーション型は
scala.annotation.StaticAnnotation
か
scala.annotation.ClassfileAnnotation
を継承することで永続化することができる。
その結果、アノテーション型のインスタンスはクラスファイル内の特別な属性として保存される。
実行時リフレクションに必要なメタデータを永続化するには
scala.annotation.Annotation
を継承するだけでは不十分であることに注意してほしい。さらに、
scala.annotation.ClassfileAnnotation
を継承しても実行時には Java
アノテーションとしては認識されないことに注意してほしい。そのためには、Java でアノテーションを書く必要がある。
API は 2種類のアノテーションを区別する:
- Java アノテーション: Java コンパイラによって生成された定義に付加されたアノテーション、つまりプログラムの定義に付けられた
java.lang.annotation.Annotation
のサブタイプ。Scala リフレクションによって読み込まれるとscala.annotation.ClassfileAnnotation
トレイトが自動的に全ての Java アノテーションに追加される。 - Scala アノテーション: Scala コンパイラによって生成された定義や型に付加されたアノテーション。
Java と Scala のアノテーションの違いは
scala.reflect.api.Annotations#Annotation
に顕著に現れており、これは
scalaArgs
と javaArgs
両方を公開する。
scala.annotation.ClassfileAnnotation
を継承する
Scala または Java アノテーションに対しては scalaArgs
は空で
(もしあれば) 引数は javaArgs
に保持される。他の全ての Scala
アノテーションの場合は、引数は scalaArgs
に保持され、javaArgs
は空となる。
scalaArgs
内の引数は型付けされた構文木として表される。
これらの構文木はタイプチェッカより後のどのフェーズにおいても変換されないことに注意する必要がある。
javaArgs
内の引数は scala.reflect.api.Names#Name
から
scala.reflect.api.Annotations#JavaArgument
へのマップとして表現される。
JavaArgument
のインスタンスは Java アノテーションの引数の様々な型を表現する:
- リテラル (プリミティブ型と文字列の定数)
- 配列
- 入れ子になったアノテーション
名前
名前 (name) は文字列の簡単なラッパーだ。
Name
には 2つのサブタイプ TermName
と TypeName
があり (オブジェクトやメンバーのような) 項の名前と
(クラス、トレイト、型メンバのような) 型の名前を区別する。同じオブジェクト内に同名の項と型が共存することができる。別の言い方をすると、型と項は別の名前空間を持つ。
これらの名前はユニバースに関連付けられている。具体例を使って説明しよう。
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> val mapName = TermName("map")
mapName: scala.reflect.runtime.universe.TermName = map
上のコードでは、実行時リフレクション・ユニバースに関連付けられた Name
を作成している。
(これはパス依存型である reflect.runtime.universe.TermName
が表示されていることからも分かる。)
名前は型のメンバの照会に用いられる。例えば、List
クラス内で宣言されている (項である) map
メソッドを検索するには以下のようにする:
scala> val listTpe = typeOf[List[Int]]
listTpe: scala.reflect.runtime.universe.Type = scala.List[Int]
scala> listTpe.member(mapName)
res1: scala.reflect.runtime.universe.Symbol = method map
型メンバを検索するには TypeName
を代わりに使って member
を呼び出す。
暗黙の変換を使って文字列から項もしくは型の名前に変換することもできる:
scala> listTpe.member("map": TermName)
res2: scala.reflect.runtime.universe.Symbol = method map
標準名
Scala のプログラムにおいて、「_root_
」のような特定の名前は特殊な意味を持つ。
そのため、それらは Scala の構造物をリフレクションを用いてアクセスするのに欠かすことができない。
例えば、リフレクションを用いてコンストラクタを呼び出すには標準名 (standard name)
universe.termNames.CONSTRUCTOR
を用いる。これは、JVM 上でのコンストラクタ名である項名「<init>
」を指す。
- 「
<init>
」、「package
」、「_root_
」のような標準項名 (standard term names) と - 「
<error>
」、「_
」、「_*
」のような標準型名 (standard type names)
の両方が存在する。
「package
」のようないくつかの名前は型名と項名の両方が存在する。
標準名は Universe
クラスの termNames
と typeNames
というメンバとして公開されている。
全ての標準名の仕様は API doc を参照。
スコープ
スコープ (scope) は一般にある構文スコープ内の名前をシンボルに関連付ける。 スコープは入れ子にすることもできる。リフレクション API で公開されているスコープの基底型は Symbol の iterable という最小限のインターフェイスのみを公開する。
追加機能は
scala.reflect.api.Types#TypeApi
内で定義されている member
と decls
が返すメンバスコープ (member scope) にて公開される。
scala.reflect.api.Scopes#MemberScope
は sorted
メソッドをサポートしており、これはメンバを宣言順にソートする。
以下に List
クラスでオーバーライドされている全てのシンボルのリストを宣言順に返す具体例をみてみよう:
scala> val overridden = listTpe.decls.sorted.filter(_.isOverride)
overridden: List[scala.reflect.runtime.universe.Symbol] = List(method companion, method ++, method +:, method toList, method take, method drop, method slice, method takeRight, method splitAt, method takeWhile, method dropWhile, method span, method reverse, method stringPrefix, method toStream, method foreach)
Expr
構文木の基底型である scala.reflect.api.Trees#Tree
の他に、型付けされた構文木は
scala.reflect.api.Exprs#Expr
型によっても表すことができる。
Expr
は構文木と、その構文木の型に対するアクセスを提供するための型タグをラッピングする。
Expr
は主にマクロのために便宜的に型付けられた構文木を作るために使われる。多くの場合、これは
reify
と splice
メソッドが関わってくる。
(詳細はマクロを参照)
フラグとフラグ集合
フラグ (flag) は
scala.reflect.api.Trees#Modifiers
である flags
を用いて定義を表す構文木に修飾子を与えるのに使われる。
以下に修飾子を受け付ける構文木を挙げる:
scala.reflect.api.Trees#ClassDef
。クラスとトレイト。scala.reflect.api.Trees#ModuleDef
。オブジェクト。scala.reflect.api.Trees#ValDef
。val
、var
、パラメータ、自分型注釈。scala.reflect.api.Trees#DefDef
。メソッドとコンストラクタ。scala.reflect.api.Trees#TypeDef
。型エイリアス、抽象型メンバ、型パラメータ。
例えば、C
という名前のクラスを作るには以下のように書く:
ClassDef(Modifiers(NoFlags), TypeName("C"), Nil, ...)
ここでフラグ集合は空だ。C
を private にするには、以下のようにする:
ClassDef(Modifiers(PRIVATE), TypeName("C"), Nil, ...)
垂直バー演算子 (|
) を使って組み合わせることができる。例えば、private final
クラスは以下のように書く:
ClassDef(Modifiers(PRIVATE | FINAL), TypeName("C"), Nil, ...)
全てのフラグのリストは
scala.reflect.api.FlagSets#FlagValues
にて定義されており、
scala.reflect.api.FlagSets#Flag
から公開されている。
(一般的には、これを
import scala.reflect.runtime.universe.Flag._
のようにワイルドカードインポートする。)
定義の構文木はコンパイル後にはシンボルとなるため、これらの構文木の修飾子のフラグは結果となるシンボルのフラグへと変換される。
構文木と違ってシンボルはフラグを公開しないが、isXXX
というパターンのテストメソッドを提供する
(例えば isFinal
は final かどうかをテストする)。
特定のフラグはある種類のシンボルでしか使われないため、場合によってはシンボルを
asTerm
、asType
、asClass
といったメソッドを使って変換する必要がある。
注意: リフレクションAPI のこの部分は再設計の候補に挙がっている。リフレクションAPI の将来のリリースにおいてフラグ集合が他のものと置き換わる可能性がある。
定数
Scala の仕様において定数式 (constant expression) と呼ばれる式は Scala コンパイラによってコンパイル時に評価することができる。 以下に挙げる式の種類はコンパイル時定数だ。 (Scala 言語仕様 の 6.24 参照):
- プリミティブ値クラスのリテラル (Byte、 Short、 Int、 Long、 Float、 Double、 Char、 Boolean および Unit)。これは直接対応する型で表される。
- 文字列リテラル。これは文字列のインスタンスとして表される。
- 一般に scala.Predef#classOf で構築されるクラスへの参照。型として表される。
- Java の列挙要素。シンボルとして表される。
定数式の用例としては
- 構文木内のリテラル (
scala.reflect.api.Trees#Literal
参照) - Java のクラスファイルアノテーションへ渡されるリテラル (
scala.reflect.api.Annotations#LiteralArgument
参照)
などがある。具体例をみてみよう。
Literal(Constant(5))
上の式は Scala ソース内での整数リテラル 5
を表す AST を構築する。
Constant
は「仮想ケースクラス」の一例で、普通のクラスなのだが、あたかもケースクラスであるかのうように構築したりパターンマッチしたりすることができる。
Literal
と LiteralArgument
の両方ともがリテラルのコンパイル時定数を返す
value
メソッドを公開する。
具体例で説明しよう:
Constant(true) match {
case Constant(s: String) => println("A string: " + s)
case Constant(b: Boolean) => println("A Boolean value: " + b)
case Constant(x) => println("Something else: " + x)
}
assert(Constant(true).value == true)
クラス参照は scala.reflect.api.Types#Type
のインスタンスを用いて表される。
この参照は、scala.reflect.runtime.currentMirror
のような
RuntimeMirror
の runtimeClass
メソッドを用いてランタイムクラスへと変換することができる。
(Scala コンパイラがクラス参照の処理を行なっている段階においては、その参照が指すランタイムクラスがまだコンパイルされていない可能性があるため、このように型からランタイムクラスへと変換することが必要となる。)
Java の列挙要素への参照はシンボル (scala.reflect.api.Symbols#Symbol
)
として表され、対応する列挙要素を JVM 上で返すことができるメソッドを持つ。
RuntimeMirror
を使って対応する列挙型や列挙値への参照の実行時の値をインスペクトすることができる。
具体例をみてこう:
// Java ソース:
enum JavaSimpleEnumeration { FOO, BAR }
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface JavaSimpleAnnotation {
Class<?> classRef();
JavaSimpleEnumeration enumRef();
}
@JavaSimpleAnnotation(
classRef = JavaAnnottee.class,
enumRef = JavaSimpleEnumeration.BAR
)
public class JavaAnnottee {}
// Scala ソース:
import scala.reflect.runtime.universe._
import scala.reflect.runtime.{currentMirror => cm}
object Test extends App {
val jann = typeOf[JavaAnnottee].typeSymbol.annotations(0).javaArgs
def jarg(name: String) = jann(TermName(name)) match {
// Constant is always wrapped in a Literal or LiteralArgument tree node
case LiteralArgument(ct: Constant) => value
case _ => sys.error("Not a constant")
}
val classRef = jarg("classRef").value.asInstanceOf[Type]
println(showRaw(classRef)) // TypeRef(ThisType(), JavaAnnottee, List())
println(cm.runtimeClass(classRef)) // class JavaAnnottee
val enumRef = jarg("enumRef").value.asInstanceOf[Symbol]
println(enumRef) // value BAR
val siblings = enumRef.owner.typeSignature.decls
val enumValues = siblings.filter(sym => sym.isVal && sym.isPublic)
println(enumValues) // Scope {
// final val FOO: JavaSimpleEnumeration;
// final val BAR: JavaSimpleEnumeration
// }
val enumClass = cm.runtimeClass(enumRef.owner.asClass)
val enumValue = enumClass.getDeclaredField(enumRef.name.toString).get(null)
println(enumValue) // BAR
}
プリティプリンタ
Trees
と
Types
を整形して表示するユーティリティを説明しよう。
構文木の表示
show
メソッドは、リフレクションオブジェクトを整形して表示する。
この形式は Scala コードの糖衣構文を展開して Java のようしたものを提供する。具体例をみていこう:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> def tree = reify { final class C { def x = 2 } }.tree
tree: scala.reflect.runtime.universe.Tree
scala> show(tree)
res0: String =
{
final class C extends AnyRef {
def <init>() = {
super.<init>();
()
};
def x = 2
};
()
}
showRaw
メソッドは、Scala の構文木 (AST) のようなリフレクションオブジェクトの内部構造を表示する。
これは Scala のタイプチェッカが見るものと同じものだ。
ここで注意すべきなのは、この形式どおりに構文木を構築すればマクロの実装でも使えるのじゃないかと思うかもしれないが、うまくいかないことが多いということだ。これはシンボルについての情報などが完全には表示されていないためだ (名前だけが表示される)。 そのため、妥当な Scala コードがあるときにその AST をインスペクトするのに向いていると言える。
scala> showRaw(tree)
res1: String = Block(List(
ClassDef(Modifiers(FINAL), TypeName("C"), List(), Template(
List(Ident(TypeName("AnyRef"))),
emptyValDef,
List(
DefDef(Modifiers(), termNames.CONSTRUCTOR, List(), List(List()), TypeTree(),
Block(List(
Apply(Select(Super(This(typeNames.EMPTY), typeNames.EMPTY), termNames.CONSTRUCTOR), List())),
Literal(Constant(())))),
DefDef(Modifiers(), TermName("x"), List(), List(), TypeTree(),
Literal(Constant(2))))))),
Literal(Constant(())))
showRaw
はインスペクトしたものの scala.reflect.api.Types
を併記することができる。
scala> import scala.tools.reflect.ToolBox // requires scala-compiler.jar
import scala.tools.reflect.ToolBox
scala> import scala.reflect.runtime.{currentMirror => cm}
import scala.reflect.runtime.{currentMirror=>cm}
scala> showRaw(cm.mkToolBox().typeCheck(tree), printTypes = true)
res2: String = Block[1](List(
ClassDef[2](Modifiers(FINAL), TypeName("C"), List(), Template[3](
List(Ident[4](TypeName("AnyRef"))),
emptyValDef,
List(
DefDef[2](Modifiers(), termNames.CONSTRUCTOR, List(), List(List()), TypeTree[3](),
Block[1](List(
Apply[4](Select[5](Super[6](This[3](TypeName("C")), typeNames.EMPTY), ...))),
Literal[1](Constant(())))),
DefDef[2](Modifiers(), TermName("x"), List(), List(), TypeTree[7](),
Literal[8](Constant(2))))))),
Literal[1](Constant(())))
[1] TypeRef(ThisType(scala), scala.Unit, List())
[2] NoType
[3] TypeRef(NoPrefix, TypeName("C"), List())
[4] TypeRef(ThisType(java.lang), java.lang.Object, List())
[5] MethodType(List(), TypeRef(ThisType(java.lang), java.lang.Object, List()))
[6] SuperType(ThisType(TypeName("C")), TypeRef(... java.lang.Object ...))
[7] TypeRef(ThisType(scala), scala.Int, List())
[8] ConstantType(Constant(2))
型の表示
show
メソッドは型を可読な文字列形式で表示することができる:
scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._
scala> def tpe = typeOf[{ def x: Int; val y: List[Int] }]
tpe: scala.reflect.runtime.universe.Type
scala> show(tpe)
res0: String = scala.AnyRef{def x: Int; val y: scala.List[Int]}
scala.reflect.api.Trees
のための showRaw
同様に、
scala.reflect.api.Types
のための showRaw
は Scala タイプチェッカが使う Scala AST を表示する。
scala> showRaw(tpe)
res1: String = RefinedType(
List(TypeRef(ThisType(scala), TypeName("AnyRef"), List())),
Scope(
TermName("x"),
TermName("y")))
この showRaw
メソッドにはデフォルトでは false
になっている名前付きパラメータ printIds
と printKinds
を持つ。
true
を渡すことで showRaw
はシンボルのユニークID
とシンボルの種類 (パッケージ、型、メソッド、getter その他) を表示することができる。
scala> showRaw(tpe, printIds = true, printKinds = true)
res2: String = RefinedType(
List(TypeRef(ThisType(scala#2043#PK), TypeName("AnyRef")#691#TPE, List())),
Scope(
TermName("x")#2540#METH,
TermName("y")#2541#GET))
位置情報
位置情報 (Position
)
はシンボルや構文木のノードの出処を追跡するのに使われる。警告やエラーの表示でよく使われ、プログラムのどこが間違ったのかを正確に表示することができる。位置情報はソースファイルの列と行を表す。
(ソースファイルの初めからのオフセットは「ポイント」と呼ばれるが、これは便利ではないことがある)
位置情報はそれが指す行の内容も保持する。全ての構文木やシンボルが位置情報を持つわけではなく、ない場合は
NoPosition
オブジェクトで表される。
位置情報はソースファイルの 1文字を指すこともできれば、文字の範囲を指すこともできる。
前者の場合はオフセット位置情報 (offset position)、後者の場合は範囲位置情報
(range position) が使われる。範囲位置情報は start
と end
オフセットを保持する。
start
と end
オフセットは focusStart
と focusEnd
メソッドを用いて「フォーカス」することができ、これは位置情報を返す
(範囲位置情報では無い位置情報に対して呼ばれた場合は this
を返す) 。
位置情報はいくつかのメソッドを使って比較することができる。
precedes
メソッドは、2つの位置情報が定義済みであり (つまり NoPosition
ではない)、かつ
this
の位置情報の終点が与えられた位置情報の始点を超えない場合に真を返す。
他にも、範囲位置情報は (includes
メソッドを用いて) 包含関係を調べたり、
(overlaps
メソッドを用いて) 交差関係を調べることができる。
範囲位置情報は透明 (transparent) か非透明 (opaque) だ。 範囲位置情報を持つ構文木は以下の不変条件を満たす必要があるため、範囲位置情報が透明か非透明であるかは許可される用法に関わってくる:
- オフセット位置情報を持つ構文木は範囲位置情報を持つ部分木を持ってはいけない。
- 範囲位置情報を持つ構文木が範囲位置情報を持つ部分木を持つ場合、部分木の範囲は親の範囲に包含されなくてはいけない。
- 同じノードの部分木の非透明な範囲位置情報同士は交差してはいけない。 (このため、交差は最大で単一の点となる)
makeTransparent
メソッドを使って非透明な範囲位置情報を透明な他は何も変わらないものに変換することができる。