EnumConstantの初期化と解釈(メモ)

public enum Hoge {
    A,
    B(1),
    C { public String toString(){ return "c"; } },
    D(1) { public String toString(){ return "d-1"; } },
    ;
    ...
}

上記のような列挙型をJavaで宣言したと仮定して、A~Dの「値」についてどう扱うか考えてました。
Tiger以降に出現したenum型は、Effective Javaにあるtype safe enumパターン(に直列化やらなにやらいろいろと機能を加えたもの)のシンタックスシュガーになっています。
つまり、列挙型の各定数はVM側から見るとpublic static finalなフィールドのようなもので、型は違えど下記も似たようなものです。

public class Foo {
    public static final String A = "aaa";
    public static final String B = "bbb";
    public static final String C = "ccc";
    public static final String D = "ddd";
}

列挙型定数の場合と違い、上記のA~Dが持つ「値」は自明です。対して、列挙型定数は現在のJavaコンパイラの実装では、下記のようにtype safe enumで書いた場合と酷似したバイトコードを生成*1します。

public class Hoge {
    public static final Hoge A = new Hoge();
    public static final Hoge B = new Hoge(1);
    public static final Hoge C = new Hoge(){ public String toString(){ return "c"; } };
    public static final Hoge D = new Hoge(1){ public String toString(){ return "d-1"; } };
    ...
}

上記のように、Hoge#[A-D]はインスタンス生成式を利用してしまうため、道を外れた方法で「値」を再現しようとすると、定数値なのに同一性の観点でうまくいきません*2。さらに言うと、C, Dは匿名クラスを作っているので、Hoge.C.getClass() と Hoge.D.getClass() ですら同一でないはずです。

なぜこんなに「値」にこだわるかというと、IrenkaのDOM要素にはEnumを利用しているものがあり、クエリ内でその比較演算を行ったりするからです。

/**
 * @when expr.operator = {@link InfixOperator#PLUS}
 */

上記を解釈すると、

  • expr.operator は InfixOperator.PLUS に格納された「値」
  • {@link InfixOperator#PLUS}は InfixOperator.PLUS という「シンボル」

という具合になってしまいます。しかも、expr.operatorというのはIrenkaの特性上、通常のJavaVM上で表現された「(InfixOperatorのインスタンス)値」となっていますが、{@link InfixOperator#PLUS}のほうはIrenkaが管理する「(InfixOperator.PLUSを表現する)シンボル」のひとつなので、互換性がありません。

他にも、Hackイベントの分類に列挙型定数を利用することを想定した場合、これらのプロパティが返すモノが「値」なのか「シンボル」なのかは重要になりそうな感じです。

幸い、java.lang.Enumの設計によりInfixOperator.PLUSをJavaVM上で評価した際の「値」からシンボルを特定することはできるので、expr.operatorという項の解釈を「値」ではなく「シンボル」とするという方法はあります。

で、「シンボル」と解釈した場合、今度は次の問題が生まれます。

@Bar(InfixOperator.PLUS) int f;

という宣言の注釈を考えた場合、Bar.valueの「評価結果」はInfixOperator.PLUSが持つ「値」になるということは疑いないと思います。
しかし、AST(DOM)という観点で考えると、Bar.valueの右辺値は「「評価結果がInfixOperator.PLUSの持つ値」になるようなノード」、というものを考える必要があり、そのまま値を突っ込むわけにいきません。

現在のIrenkaのDOMにおけるBar.valueの構造は、簡略表記すると下記のようなイメージです。

<CtAnnotationInstance>
  <simpleName>Bar</simpleName>
  <elements>
    <CtAnnotationInstanceElement>
      <spec>(value)</spec>
      <value> <!-- @Barの右辺値を表すノード -->
        <CtVariableAccess>
          <variable>
            <CtEnumConstant>
              <master>(InfixOperation.PLUS)</master>
            </CtEnumConstant>
          </variable>
        </CtVariableAccess>
      </value>
    </CtAnnotationInstanceElement>
  </elements>
</CtAnnotationInstance>

@Barの右辺値は、上記にあるようにCtEnumConstantによるシンボルの表記でなく、それに対する「参照」を行うノードCtVariableAccesによってラップされた構造として表現されます*3

少し前の、「expr.operator = {@link InfixOperator#PLUS}」のleft hand sideを「シンボル」として扱う場合、今度は「シンボル」そのものか「シンボルへの参照」なのかが問題になります。

@Bar(InfixOperator.PLUS) int f;

/**
 * @when
 *     ?bar in {@link #f}.annotations
 *     ?element in ?bar.elements
 *     element.value = {@link InfixOperator#PLUS}
 */

上記は次のように解釈されます。

  1. ?bar は フィールドfの注釈一覧に含まれる (: @Bar(InfixOperator.PLUS))
  2. ?element は ?barの要素一覧に含まれる (: value = InfixOperator.PLUS)
  3. ?element の右辺は InfixOperator.PLUS に等しい

さらに厳密に解釈すると、「?element の右辺」は「シンボル」なのか「シンボルへの参照」なのかを規定する必要があります。他にもクエリ中の「{@link InfixOperator#PLUS}」という項が「シンボル」なのか「シンボルへの参照」なのか、もしくは「?element の右辺」に合わせて推移するのか。問題は山積みです。

ということをいちいち考慮してクエリを書くのも漢らしいといえばそうかもしれませんが、このような問題を意識しないでクエリをかけるような演算子も必要かなと思います。この辺に関しては次回以降に。

*1:厳密にはクラス初期化子が作成され、そこの中でインスタンスが生成されます

*2:道を外す方法が標準で存在しないことこそがtype safe enumパターンの強みでもあります

*3:この辺は議論の余地あり