Java に独自の型検査処理を追加する方法

これは 言語実装 Advent Calendar 2015 の 14 日目の記事です.

Pluggable Annotation Processing API (JSR 269) による Functor の型検査 という以前に書いた記事を, Advent Calendar 用に再構成, 要約したものです.

Note

この記事では主に型情報にアクセスする方法について書きます. それ以外の部分は上記の元記事を参照してください.

Java で函手を書いてみたかった

そもそもは「Java で函手を書く」という無茶をしてみたかっただけです. 無茶は承知の上で, 書こうとする過程でどんな障害があって, どうやって回避できるかを探りたかったのです.

函手について解説するのはこの記事の目的ではないので, 厳密な定義などについては触れません. 函手 (関手, Functor) の定義は 独習 Scalaz — 圏の例 あたりを読んでください.

何をしたかったかをソースコードで表せば,

import java.util.function.Function;

public class MyFunctor<A> {

    public <B> MyFunctor<B> map(Function<A, B> morphism) {
        // ...
    }

}

こんなメソッド map を持ったクラスを実装するのがゴールです.

型制約が上手く表現できない

書き始めてすぐに引っ掛かるのは型制約のところです. Java では継承によって型制約を表現することが多く, 素直に考えてみると

import java.util.function.Function;

interface Functor<A> {

    <B> Functor<B> map(Function<A, B> morphism);

}

こんなインターフェースで函手を表現すると思います. (引用元: https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_interface/src/main/java/co/coatomo/math/categorical/typeclass/functor/Functor.java?at=default&fileviewer=file-view-default)

しかし, これでは不十分なところがあります.

Functor インターフェースを実装した FList クラスと WrongOption クラスがあったとします. ここで (名前の通り) WrongOption#map の実装を間違えて, 返り値の型が FList<B> になってしまったとしましょう.

public class WrongOption<A> implements Functor<A> {

    // 返り値の型が変だがコンパイルは通る!!
    @Override
    public <B> FList<B> map(Function<A, B> morphism) {

        // TODO Auto-generated method stub
        return null;
    }

}

実はこれでもコンパイルは通るのです.

では Functor#map メソッドの返り値の型を F extends Functor みたいにパラメータ化すればいいかと言うと, それでも上手く行きません. 函手は型パラメータを1つ取るのですが, 型パラメータを取る型パラメータを返り値の型に指定すると javac さんに怒られてしまいます. (これは「Java は kind を扱えない」ということなのでしょう.)

それなら自分で型検査しよう

さて, やりたい型検査と javac さんの能力が上手く一致しない状況になっていますが, それなら javac さんの型検査とは別の独自の型検査を追加すればいいはずです.

そのように javac の型検査に介入する方法として, Pluggable Annotation Processing API (JSR 269) があります. JSR 269 を使うと, 注釈 (annotation) が付けられた対象 (クラス, メソッドなど) のリストと, それぞれの対象を含む AST の走査が行えます. (原則的には AST の変更は行えないはずですが, 現状の実装では変更できちゃうみたいです. lombok はそれを利用しているようです.)

今回使う範囲の JSR 269 の機能については以下のページを参照してください. (Java の有名な情報源の1つと言えばやはり @skrb さんの連載ですよね.)

実装内容

注釈と注釈処理機の準備

こんなふうに注釈を用意して

@Target(ElementType.TYPE)
public @interface Functor {

    @Target(ElementType.METHOD)
    public @interface Map {

    }
}

(引用元: https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_annotation/src/main/java/co/coatomo/math/categorical/typeclass/annotation/Functor.java?at=default&fileviewer=file-view-default#Functor.java-13:20)

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({ "co.coatomo.math.categorical.typeclass.annotation.Functor" })
public class FunctorAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        if (annotations.isEmpty()) {
            return true;
        }

        Messager messager = super.processingEnv.getMessager();
        Types typeUtils = super.processingEnv.getTypeUtils();

        for (TypeElement annotation : annotations) {
            for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(annotation)) {

                DeclaredType mappedMorphismDomain = getMappedMorphismDomain(annotatedElement, messager);

                ExecutableElement mapMethod = getMapMethod(mappedMorphismDomain, messager);

                ExecutableType map = ExecutableType.class.cast(mapMethod.asType());

                DeclaredType morphismType = getMorphismType(map, mapMethod, messager);

                TypeVariable morphismCodomain = getMorphismCodomain(map, mapMethod, messager);

                DeclaredType mappedMorphismCodomain = getMappedMorphismCodomain(map, mapMethod, messager);

                verifyDiagram(mappedMorphismDomain, morphismType, morphismCodomain, mappedMorphismCodomain, typeUtils,
                        mapMethod, messager);
            }
        }

        return true;
    }

(引用元: https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_annotation/src/main/java/co/coatomo/math/categorical/typeclass/processor/FunctorAnnotationProcessor.java?at=default&fileviewer=file-view-default#FunctorAnnotationProcessor.java-50:85)

こんな感じに注釈処理機 (annotation processor) を実装します. (FunctorAnnotationProcessor の全体の実装はリンク先を見てみてください.)

実装の一例

JSR 269 の使用例として getMappedMorphismDomain メソッドの実装を見てみましょう.

/**
 * Get F(A).
 *
 * @param annotatedElement
 * @param messager
 * @return
 */
private DeclaredType getMappedMorphismDomain(Element annotatedElement, Messager messager) {

    DeclaredType mappedMorphismDomain = DeclaredType.class.cast(annotatedElement.asType());
    List<? extends TypeMirror> typeArguments = mappedMorphismDomain.getTypeArguments();
    if (typeArguments.size() != 1) {
        messager.printMessage(Kind.ERROR, "a number of type parameters must be one", annotatedElement);
    }

    return mappedMorphismDomain;
}

(引用元: https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_annotation/src/main/java/co/coatomo/math/categorical/typeclass/processor/FunctorAnnotationProcessor.java?at=default&fileviewer=file-view-default#FunctorAnnotationProcessor.java-87:103)

Element インターフェースが AST のノードを表していて, Element#asType メソッドによってそのノードの型が取得できます. さらに DeclaredType#getTypeArguments で型パラメータのリストが取得できます.

ここで出てくる messager というのは, ProcessingEnvironment#getMessager というメソッドで取得できる Messager オブジェクトです. このオブジェクトを通して, 注釈処理に関するログ出力が行えます. 失敗のログを出した場合, コンパイル処理自体を異常終了させてくれる便利な仕組みになっています.

型検査に必要な道具

ProcessingEnvironment オブジェクトは, 事前に AbstractProcessor#processing フィールドに設定されているので, それを使うだけで済みます.

ProcessingEnvironment#getTypeUtils で取得できる Types オブジェクトは型の検査を行うのに必要なツールです.

その一例として FunctorAnnotationProcessor#verifyDiagram の実装での Types#isSameType メソッドの使い方を見てみましょう.

/*
 * domain of f must be A.
 */
TypeMirror expectedMorphismDomain = mappedMorphismDomain.getTypeArguments().get(0);
TypeMirror actualMorphismDomain = morphismType.getTypeArguments().get(0);
if (!typeUtils.isSameType(expectedMorphismDomain, actualMorphismDomain)) {
    messager.printMessage(Kind.ERROR, MessageFormat.format(
            "a type of the map method parameter must be {0}<{1}, {2}>",
            java.util.function.Function.class.getCanonicalName(), expectedMorphismDomain,
            expectedMorphismCodomain), mapMethod.getParameters().get(0));
}

(引用元: https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_annotation/src/main/java/co/coatomo/math/categorical/typeclass/processor/FunctorAnnotationProcessor.java?at=default&fileviewer=file-view-default#FunctorAnnotationProcessor.java-194:204)

ここでは TypeMirror#equals ではなく, Types#isSameType を使って型の同一性を検査しています. Javadoc によると, ある2つの Element オブジェクトの型がその時点でのコンテキストで同じだとしても, TypeMirror がオブジェクトとして異なることがあるそうです. そのため, コンテキストを考慮した比較のために Types#isSameType を使う必要があるのです.

めでたしめでたし

インターフェースでは実現できなかった型検査の処理を, JSR 269 を使って無事に実装できました. しかしインターフェースを使わなかった代償として, 函手に共通の処理でも, それぞれの函手クラスごとに処理を実装するか, もしくは, リフレクションで map メソッドを呼び出さなければなりません.

まぁ, こういう実際の実装で苦しむところが分かっただけでも, 良しとしましょう.

ここまでで解説したのは JSR 269 が提供する機能やクラスのほんの一部ですが, FunctorAnnotationProcessor の実装で使っている機能はほぼ網羅しています. JSR 269 の残りの機能については, 上で挙げた @skrb さんの連載や Javadoc を読んでください.

それでは.