Scala アノテーション (Internals of Scala Annotations)
目次
1 はじめに (Introduction)
2 生成定義上のアノテーション (Annotations on synthetics)
- 2.1 フィールド、ゲッターとセッター (Fields, Getters and Setters)
- 2.2 オブジェクト (Objects)
- 2.3 トレイト (Traits)
3 内部表現 (Internal Representation)
- 3.1 型チェックの前 (Before Type-Checking)
- 3.2 型チェックの後 (After Type-Checking)
4 アノテーション探訪 (Visiting Annotations)
- 4.1 ツリー:トラバーサ/トランスフォーマ (Trees : Traverser/Transformer)
- 4.2 型:TypeMap (Types : TypeMap)
5 バイトコード中のアノテーション (Annotations in bytecode)
- 5.1 Scala アノテーション (Scala annotations)
- 5.2 Java クラスファイルアノテーション (java classfile annotations)
- 5.2.1 Java クラスファイルアノテーションの解析 (Parsing of java classfile annotations)
6 FAQ
7 事前定義アノテーション (Pre-defined annotations)
1 はじめに (Introduction)
このドキュメントは Scala コンパイラにおいてアノテーションがどのように表現され、処理されるか記述します。この内容は、アノテーションを使う Scala コンパイラプラグインの開発者だけでなく、コンパイラの開発者自身にとっても重要です。
Scala には 2 種類のアノテーションがあります。:シンボルアノテーションと型アノテーションです。シンボルアノテーションは、宣言または定義、例えば、クラス、メソッド、フィールド、型メンバー、ローカル値(local values)、パラメータあるいは型パラメータに付けることができます。:
@ann class C[@ann T](@ann x: T) {
@ann def f = {
@ann val x = 1
x
}
}
型アノテーションは、型に付けられます(対応する型は「アノテーション付き」と言われます)。帰属(ascription)構文を使う式上のアノテーションは、内部的には、推論された式の型に対するアノテーションとして表されることに注意してください。
def f(x: Int @ann) = { // 型アノテーション
val r = (x + 3): @ann // 式の型 : ''Int @ann''
val s = 4: Int @ann
r + s
}
次章では、2 種類のアノテーションがコンパイラの異なるフェーズにおいてどのように表されるか、アノテーションがどのようにバイトコードにセーブされるか、そして Java アノテーションとの互換性がどのようにして得られるか説明します。
2 生成定義上のアノテーション
(Annotations on synthetics)
多くの場合、Scala コンパイラは与えられたソース定義に対して追加のエンティティを作成します。例えば、公開フィールドに対するゲッターとセッター、あるいはオブジェクトのメソッドに対する静的フォワーダ(static forwarder)です。本章では、アノテーションがそれら生成定義(synthetic definition)へどのようにコピーされるか説明します。
2.1 フィールド、ゲッターとセッター
(Fields, Getters and Setters)
Scala では一つのフィールド定義が、取り囲むクラス中に 6 つのエンティティを生成することがあります。:
- 実際の(非公開)フィールド。
- ゲッターおよびセッターメソッド。
- もしフィールドが @BeanProperty あるいは @BooleanBeanProperty のアノテーション付きなら、ビーン式ゲッターおよびセッターメソッド。
- もしフィールドが、例えば class(val x: Int)のように、クラスの val-パラメータとして定義されているなら、コンストラクタパラメータ。
デフォルトでは、フィールド上のアノテーションは実際のフィールドにだけ加えられます。この振る舞いはパッケージ scala.annotation.target 中のメタアノテーション(@field、@getter、@setter、@beanGetter、@beanSetter、@param)を使って制御できます。
これらメタアノテーションを使う 1 つの場面は、それらをフィールドアノテーションのアノテーション型に付けることです。例えば次の定義では、アノテーション @Id は、最終的にはコンパイラが生成するビーン式ゲッター getX() (フィールド上ではありません)に付けられます。
class C {
@(javax.persistence.Id @beanGetter) @beanProperty val x = 1
}
構文をより軽量にしコード複製を減らすために、アノテーション型に対して型エイリアスを定義し、そこにターゲットアノテーションを加えても構いません。:
object ScalaJPA {
type Id = javax.persistence.Id @beanGetter
}
class A {
@ScalaJPA.Id @BeanProperty val x = 0
}
Scala で定義されたアノテーションについて、メタアノテーションを用いてアノテーションクラス自身をアノテートすることで、デフォルトのアノテーションターゲットを記述できます。
@getter @setter class myAccessorAnnot extends StaticAnnotation
2.2 オブジェクト (Objects)
The members of an object named t are compiled into a classfile named t$ (the module class). Additionally, if the object is not nested (i.e. defined inside a class or another object), a companion class named t is generated for Java compatibility which contains static forwarder methods to t$.
t という名前のオブジェクトのメンバーは、t$ という名前のクラスファイルへコンパイルされます(モジュールクラス)。さらに、もしオブジェクトがネストされていなければ(つまり、クラスあるいは他のオブジェクト内で定義されている)、Java との互換性のために、t$ への静的フォワーダメソッドを含む名前 t のコンパニオンクラスが作成されます。 これら静的フォワーダは、モジュールクラスにおけるそれら対応物と同じアノテーションを持っています。
注意 : 静的フォワーダは Scala コンパイラのバックエンドで作成され、コンパイラシンボルはありません。ですから、Java アノテーションとしてバイトコードに書き込まれるクラスファイルアノテーション以外では、それらのアノテーションを見ることはありません。
オブジェクトそれ自身のアノテーションは、コンパニオンクラス(t)の型シンボルにではなく、モジュールの項シンボル(term symbol)とモジュールクラス(バイトコードの t$)の型シンボルに加えられます。; ですから静的フォワーダを含むクラスは、オブジェクトのアノテーションを持ちません。
同様に、ケースクラス C 上のアノテーションは、コンパニオンオブジェクト C あるいはそのモジュールクラス C$ 上にではなく、クラスシンボルとクラスファイル C 中にあります。
2.3 トレイト (Traits)
トレイト T は通常、インターフェース T と、T の具象メンバーの実装を含む名前 T$class の実装クラスへコンパイルされます。 T の定義上のアノテーションは、実装クラス中の対応する定義にコピーされます。
トレイトを拡張しているクラスのミックスインされたメンバーは、トレイト中のそれらの対応物と同じアノテーションを持ちます。
トレイト自身に対するアノテーションは、実装クラスにコピーされません。
2.3.1 例 (Example)
次は、クラスファイルアノテーション Ann を使う例です。 Scala ソースコードは次です。:
@Ann trait T {
@Ann def f: Int = 0
@Ann var x: Int = 1
}
class C extends T
コンパイルされたコード(java 構文)は、次の形になります。:
@Ann interface T {
@Ann int f();
@Ann int x();
@Ann void x_=(int y);
}
class T$class {
@Ann static int f() { return 0; }
}
class C extends T {
@Ann int _x = 1 // mixed-in field
@Ann int x() { return _x; } // mixed-in getter
@Ann void x_=(int y) { _x = y; } // mixed-in setter
@Ann int f() { return T$class.f(); } // mixed-in method f
}
3 内部表現 (Internal Representation)
3.1 型チェックの前 (Before Type-Checking)
型チェックの前に、アノテーションは AST(抽象構文木) の一部として表現されます。; アノテーションは単なるコンストラクタ呼び出しであることに注意してください。 シンボルアノテーションは、例えば次のように、定義の Modifiers に保存されます。:
@ann def f: Int = 1
// DefDef(Modifiers(flags = 0,
// annots = List(Apply(Select(New(Ident("ann")), "<init>"),
// List())))), // 引数なし
// "f", // 名前
// List(), // 型パラメータなし
// List(), // 値パラメータなし
// Int, // 戻り値型
// Literal(Constant(1)))
パーサーは、型アノテーションおよびアノテーション帰属構文(ascription)に対して、もし複数のアノテーションがあるときには、ネスト可能な Annotated ツリーを生成します。
def f: Int @ann = 1
// DefDef(NoMods,
// "f", // 名前
// List(), // 型パラメータなし
// List(), // 値パラメータなし
// Annotated(Apply(Select(New(Ident("ann")), "<init>"), List()),
// Int),
// Literal(Constant(1)))
3.2 型チェックの後 (After Type-Checking)
型チェックの間に、アノテーションは AST から取り除かれ、クラス AnnotationInfo のインスタンスとして、対応するシンボルあるいは型に付けられます。
case class AnnotationInfo(
atp: Type,
args: List[Tree],
assocs: List[(Name, ClassfileAnnotArg)])
An AnnotationInfo mainly consists of the annotation type tpe and the arguments args represented as compiler trees. These compiler trees are in the form as they leave the type-checking phase, i.e. they are attributed with symbols and types.
AnnotationInfo は主に、アノテーション型 tpe と、コンパイラツリーとして表わされた引数 args からなります。 これらのコンパイラツリーは、型チェックフェーズの後の形です。つまりシンボルと型で属性付けられています。 それらはコンパイラの後のフェーズで書き換えられないことに注意してください。
もしアノテーション型 atp がクラスファイルアノテーション(5.2参照)なら、3 つめの引数 assocs はアノテーション引数を表す名前/値ペアのリストです。クラスファイルアノテーションは Java アノテーションとの互換性のためだけに存在します。: Scala で定義され、かつ java 互換な方法で保持する必要のないアノテーションについては、assocs リストは常に空です。
シンボルアノテーションは、メソッド annotations:List[AnnotationInfo]を使って、対応する定義のシンボルを介してアクセスできます。:
tree match {
case dd: DefDef =>
val annots = dd.symbol.annotations
...
}
型アノテーションを伴う型は、コンパイラ中では、次の構造を持つ AnnotatedType のインスタンスとして表されます。:
case class AnnotatedType(annotations: List[AnnotationInfo],
underlying: Type,
selfsym: Symbol)
アノテーション付き型 selfsym は、実験的なフィーチャの一部であって、その使用は推奨されません。
4 アノテーション探訪 (Visiting Annotations)
4.1 ツリー : トラバーサ/トランスフォーマ
(Trees : Traverser / Transformer)
前に述べたとおり、型チェッカはアノテーションを AST から取り除きます。アノテーション引数ツリーはコンパイルされるコードの一部ではなくなるので、それは後のコンパイラフェーズでの変換を一切受けません。
勿論、アノテーション引数ツリーをシンボルの annotations あるいは AnnotatedType から抽出することで、それに対してトラバーサあるいはトランスフォーマを手作業で適用することはできます。
4.2 型 : TypeMap (Types:TypeMap)
A type map is a utility which maps a type transformation function (Type => Type) over a type and all its parts.
型マップ(type map) は、型とその全ての部分上に型変換関数(Type => Type)をマップするユーティリティです。型マップは Scala コンパイラで頻繁に使われます。アノテーション付き型の場合は、アノテーション引数ツリーもその型の一部であり、マッピングはそれらツリー中の型にも適用されます。 しかしこの振る舞いは、型マップ中のフィールド dropNonConstraintAnnotations を true に設定することで禁止できます。
This way all type annotations which are not type constraints (see below), are dropped when applying the type map .
このように、型制約(type constraints 下記参照)ではないすべての型アノテーションは、型マップを適用する時に外されます。
上記の振る舞いは、アノテーション付き型が型チェックフェーズの後で変わることを意味します。: アノテーション引数ツリー中の型は変わるかもしれず、完全に無くなることもあり得ます。同様に、型消去フェーズはすべての型アノテーションを削除することに注意してください。
TypeConstraint のサブ型であるアノテーションは、型マップ適用時には決して取り除かれません。これらのアノテーションはプラグイン化可能な型システム、例えば数字に単位を付加する型システム : Int @dim(Unit.kg)、の実装に使うことができます。:
5 バイトコード中のアノテーション
(Annotations in bytecode)
5.1 Scala アノテーション (Scala annotations)
バイトコード中にセーブされるアノテーションについて、アノテーションクラスは StaticAnnotation に適合しなければなりません。シンボルとアノテーション付き型は、対応するコードが現在コンパイル中かどうか、あるいはそれらがバイトコードから再構築されているかどうかにかかわらず、同じ静的なアノテーションを持ちます。
5.2 Java クラスファイルアノテーション
(java classfile annotations)
そのサブ型が ClassfileAnnotation であるアノテーションは、標準的な Java クラスファイルアノテーションとしてクラスファイルに書き込まれます。java で(@interface を使って)定義されたアノテーションクラスは、クラスファイルアノテーションとしてマークされるので、それらは常にこの方法で発行されます。
As required by the java platform, an argument to a classfile annotation has to be either a compile-time constant, a nested classfile annotation or an array of one of them .
java プラットフォームからの要求により、クラスファイルアノテーションへの引数は、コンパイル時定数であるネストしたクラスファイルアノテーションか、あるいはそれらの 1 つからなる配列でなければなりません。
Scala コンパイラは、アノテーションクラスの @Retention (*1) 値が何であっても、RuntimeVisibleAnnotation としてクラスファイルアノテーションを発行することに注意してください。
また、Scala コンパイラは、あらゆるアノテーションをすべてのエンティティに付けることを許し、アノテーションクラスの @Target (*2) 値をチェックしないことにも注意してください。
5.2.1 Java クラスファイルアノテーションの解析
(Parsing of java classfile annotations)
Scala コンパイラによって発行されたのではないクラスファイルを解析するとき、クラスファイルアノテーションのいくつかは読み取られ、対応するシンボルに付けられます。
- クラス、フィールド、メソッド上の RuntimeVisibleAnnotations (@Retention RUNTIMEをもつ java アノテーション) は調べられます。
- パラメータの上のクラスファイルアノテーションは調べられません。
- RuntimeInvisibleAnnotations (@Retention CLASSFILEをもつ java アノテーション) は決して調べられません。
6 FAQ
アノテーション中で補助コンストラクタの定義/使用は許されますか? はい。唯一の警告は、AnnotationInfo がどのコンストラクタが使ったかを明らかにしないということです。
アノテーションクラスは、他のアノテーションを拡張できますか? はい。
アノテーション中で名前付き引数を使うとどうなりますか? 引数が名前付きのスタイルで指定されなければならないクラスファイルアノテーションを除き、名前付き引数の使用はおそらく予期しない結果を招きます。名前付き引数を伴うアノテーションコンストラクタ呼び出しの型チェック時、それは次の形のブロックに変換されます。:
{
val x$1 = arg1
...
val x$n = argn
annot(x$1, ..., x$n)
}
このアノテーションに対する AnnotationInfo は、型チェック (x$1, ..., x$n) の後にアノテーション引数を使って構成されます。ですからアノテーションの実際の引数は失われます。
アノテーションコンストラクタ中でデフォルト引数を定義/使用できますか? はい。しかしそれは、名前付き引数を使う場合と似た予期しない結果を招きます。生成された AnnotationInfo の引数ツリーは、実際のアノテーション引数ではなく、名前付き適用ブロックのローカル値への参照です。
7 事前定義アノテーション (Pre-defined annotations)
Scala 言語仕様書[1]の 11 章で、すべての事前定義されたアノテーションをリストし、それらの振る舞いを説明しています。
参照 (References)
最終更新:2011年05月06日 09:24