Pluggable Annotation Processing API (JSR 269) による Functor の型検査

https://bitbucket.org/cocoatomo/categorical/ で開発している, 注釈による型クラスの実現手法の解説です.

発端

うらがみさんが Scala による Functor の実装をしていて,

trait Functor[A, F[_]] { self: F[A] =>
  def map[B](f: A => B): F[B]
}

(https://github.com/backpaper0/sandbox/blob/134a658c42eb14354ce53fa95931d5a32672fc74/functor-applicative-study/src/main/scala/functor-applicative-study.scala#L5-L7 より引用)

興味深かったので Java でもなんとかならないかと試していました.

実現したいこと

やりたかったのは,

  A => F(A)
f |    | F(f)
  V    V
  B => F(B)

という図式において

  • Java でできる範囲の型制約で Functor を実装する
  • 上図の A をレシーバオブジェクトとする
  • Functor#map メソッドは, 上図の f を引数とし F(B) を返り値とする

という型設計の Functor を実装することでした. Functor#mapFunction オブジェクトを返しても良かったのですが, 「こっちの方が Java っぽいかな」という感覚的な理由でこのシグネチャにしました.

最初の失敗

まずはうらがみさんの Scala のコードを真似て, Java で書いてみましたが javac さんに怒られました. 要はメソッドの返り値に「型パラメータを取る総称型」は使えないそうです. この形の継承で型制約を課そうという方針は, 上手くいかないようです. (ただし別の形の継承では, 上手く型制約を課すことはできそうです. [Kotlin_functor_ntaro])

試行錯誤した結果, 返り値を Functor<B> とすることで, 一応インターフェースの定義はできました.

package co.coatomo.math.categorical.typeclass.functor;

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 より引用)

public abstract class FList<A> implements Functor<A> {

    // ...

    @Override
    public abstract <B> FList<B> map(Function<A, B> morphism);

(https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_interface/src/main/java/co/coatomo/math/categorical/typeclass/functor/FList.java?at=default&fileviewer=file-view-default#FList.java-5:21 より引用)

しかしこれでは, Functor#map メソッドの返り値が Functor<B> となっていて, 継承したクラスのインスタンスを返すことを強制できていません. なので例えば, Functor<A> を継承した WrongOption<A> というクラスの map メソッドで, 間違って返り値の型が Functor<A> を継承した FList<A> というクラスになっていてもコンパイルが通ってしまいます.

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

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

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

}

実現手法の検討

型と型クラスの関係は所属関係, つまり「個別の型が型クラスに所属する」関係なので, Java のインターフェースを型クラスとし, その実装クラスを型として表現しようというのは自然な (ある意味, 安直な) 発想です.

この方針で上手く実装できない問題の, 根本的な原因は以下の 2 つの要因によるものです.

  • 素の javac で「ある Functor クラス Fmap メソッドの戻り値が F である」という制約を課すには, 型パラメータしか方法が無い
  • Java は返り値に型パラメータ付きの総称型を許していない

結局は javac を拡張しない限り実現できないことになります.

実装設計

じゃあ「javac を拡張すればいいじゃない」という発想になり, そこで使う手段として Pluggable Annotation Processing API (JSR 269) [JSR269] を選びました.

採用した理由としては, この API の特徴として以下のものがあるからです.

  • Java SE 6 以降の標準の API である
  • コンパイル時に動作し, 型の情報にアクセスできる
    • プリプロセッサやマクロの立ち位置とほぼ同じ
  • エラーが起きたときに javac の実行を異常終了させることができる

“java preprocessor” や “java macro” で検索すると第三者による外部ツールも見付かりますが, 継続性の面で Java の標準である JSR 269 に優位性があると判断しました.

注釈の使い方は次のようなものを考えました. おそらく多くの人にとって自然な使い方に見えると思います.

@Functor
public class ListFunctor<A> {

    @Functor.Map
    public <B> ListFunctor<B> map(Function<A, B> morphism) {

        // TODO implement
        return null;
    }
}

(https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_functor/src/main/java/co/coatomo/math/categorical/typeclass/ListFunctor.java?at=default&fileviewer=file-view-default#ListFunctor.java-7:16 より引用)

この型検査では次の条件が満たされているかを確認します.

  • @Functor 注釈の付いたクラス (以下,「函手クラス」) の型パラメータは 1 つであること
    • ここでは, この型パラメータを「始域」(または,「ドメイン」) と呼ぶことにする
      • 函手によって写される対象のうち, 射の始域の方の対象なので, この呼び方をすることにした
    • 「実現したいこと」にある図では, 函手クラスは “F”, 始域は “A” にあたる
  • 函手クラスに @Functor.Map 注釈の付いたメソッド (以下,「函手メソッド」) が 1 つだけあること
    • 「実現したいこと」にある図では “F” にあたる
  • 函手メソッドには型パラメータが 1 つだけあること
    • ここで, この型パラメータを「終域」(または,「コドメイン」) と呼ぶことにする
      • 函手によって写される対象のうち, 射の終域の方の対象なので, この呼び方をすることにした
    • 「実現したいこと」にある図では “B” にあたる
  • 函手メソッドのパラメータは, java.util.function.Function<A, B> (A は始域, B は終域) 型のパラメータが 1 つだけあること
    • 「実現したいこと」にある図では “f” にあたる
  • 函手メソッドの返り値の型は函手クラスであり, その型パラメータは終域であること

さて理論的な話はここまでにして, 具体的な開発作業の話に移りましょう.

開発環境

開発環境を作るにあたっては, “Pluggable Annotation Processing API 使い方メモ” [opengl-8080] という記事が非常に助けになりました. この記事が無かったら, 開発環境を作るのにもっと時間がかかっていました.

プロジェクト構成は以下のようにしました. src/test/java が無かったりしますが, 気にしないでください.

categorical_annotation
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   └── resources
    │       └── META-INF
    │           └── services
    │               └── javax.annotation.processing.Processor
    └── test
        └── java
categorical_functor/
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    └── main
        └── java

categorical_annotation の方が注釈と注釈処理機のプロジェクト, categorical_functor の方がその注釈を使った函手のプロジェクトです. categorical_annotation プロジェクトの成果物はローカルの Maven レポジトリにインストールし, categorical_functor 側はローカルレポジトリから取得する構成にしました.

今回作る注釈処理機をサービスプロバイダを利用して読み込ませるために, src/main/resources/META-INF/services/javax.annotation.processing.Processor という名前のファイルも用意しました. これは中身に次のように注釈処理機の FQCN が書いてあるだけのテキストファイルです.

co.coatomo.math.categorical.typeclass.processor.FunctorAnnotationProcessor

この状態で .jar ファイルを作成し, それをクラスパスに含めれば javac コマンドを実行したときに注釈処理機が動くようになります.

Eclipse の話になりますが, 両方のプロジェクトとも SpringSource の Gradle IDE のプラグインを有効にした Gradle のプロジェクトにしてあります.

Gradlesdkman でインストールしてあります. Gradle のバージョンはこんな感じです.

$ ./gradlew --version

------------------------------------------------------------
Gradle 2.7
------------------------------------------------------------

Build time:   2015-09-14 07:26:16 UTC
Build number: none
Revision:     c41505168da69fb0650f4e31c9e01b50ffc97893

Groovy:       2.3.10
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
JVM:          1.8.0_25 (Oracle Corporation 25.25-b02)
OS:           Mac OS X 10.10.5 x86_64

categorical_functor/build.gradle は特に変わったところは無いです. (ほぼ ./gradlew init 実行直後のまま)

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'eclipse'

group = 'co.coatomo.math'
archivesBaseName = 'categorical_annotation'
version = '1.0.0-SNAPSHOT'

repositories {
    jcenter()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.12'

    testCompile 'junit:junit:4.12'
}

(https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_annotation/build.gradle?at=default&fileviewer=file-view-default より引用. コメントは省略)

categorical_annotation に依存する Eclipse プロジェクトとして扱えるように categorical_annotation/build.gradle を以下のように設定しました.

ポイントは

  • ./gradlew eclipse としたときに, 注釈処理機を利用する Eclipse の設定を追加する
  • ./gradlew eclipse としたときに, SpringSource の Gradle IDE のプラグインを有効にするための設定を追加する
  • ./gradlew cleanEclipse としたときに, 独自に作成したファイルを削除すること

の 3 点です. 細かいところは “Pluggable Annotation Processing API 使い方メモ” [opengl-8080]Eclipse プロジェクト化 を読んでください.

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'eclipse'

group = 'co.coatomo.math'
archivesBaseName = 'categorical_funtor'
version = '1.0.0-SNAPSHOT'

repositories {
    jcenter()

    mavenLocal()
}

dependencies {
    compile 'org.slf4j:slf4j-api:1.7.12'

    testCompile 'junit:junit:4.12'

    compile 'co.coatomo.math:categorical_annotation:1.0.0-SNAPSHOT'
}

// for Eclipse

ext {
    eclipseAptPrefsFile = '.settings/org.eclipse.jdt.apt.core.prefs'
    eclipseFactoryPathFile = '.factorypath'
    processorJarPath = System.getProperty("user.home") + '/.m2/repository/co/coatomo/math/categorical_annotation/1.0.0-SNAPSHOT/categorical_annotation-1.0.0-SNAPSHOT.jar'
}

eclipse {
    project.name = 'categorical_functor'

    project.natures.add(0, 'org.springsource.ide.eclipse.gradle.core.nature')

    classpath.file.withXml {
        it.asNode().appendNode('classpathentry', [kind: 'src', path: '.apt_generated'])
    }

    jdt.file.withProperties { properties ->
        properties.put 'org.eclipse.jdt.core.compiler.processAnnotations', 'enabled'
    }
}

eclipseJdt << {
    file(eclipseAptPrefsFile).write """\
        |eclipse.preferences.version=1
        |org.eclipse.jdt.apt.aptEnabled=true
        |org.eclipse.jdt.apt.genSrcDir=.apt_generated
        |org.eclipse.jdt.apt.reconcileEnabled=true
        |""".stripMargin()

    file(eclipseFactoryPathFile).write """\
        |<factorypath>
        |    <factorypathentry kind="PLUGIN" id="org.eclipse.jst.ws.annotations.core" enabled="true" runInBatchMode="false"/>
        |    <factorypathentry kind="EXTJAR" id="${file(processorJarPath).absolutePath}" enabled="true" runInBatchMode="false"/>
        |</factorypath>
        |""".stripMargin()
}

cleanEclipse << {
    file(eclipseAptPrefsFile).delete()
    file(eclipseFactoryPathFile).delete()
}

(https://bitbucket.org/cocoatomo/categorical/src/454846f4b50b05ea76637445a7530a76ef9f83a6/categorical_functor/build.gradle?at=default&fileviewer=file-view-default より引用, 一部コメントは省略)

これでビルドやリリースが Gradle で管理できるようになりました.

Annotation Processor の実装

まずどれが函手なのかを示す注釈を作成します. 上で書いたように @Functor 注釈と, その内部で宣言した @Functor.Map 注釈を用意します. Map 注釈が @Functor 注釈の内部にあるのは, 両者の関係性の強さを表すためです. (本来, 対象を写すのも射を写すのも同一の函手なのですが, プログラミング言語上では型が異なることがほとんどなので, それぞれを別のものとして実装しています.)

@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 より引用)

@Functor 注釈と @Functor.Map 注釈に対する処理を実装します. ここでは AbstractProcessor クラスを継承して, process メソッドをオーバーライドします. [skrb_javase6_95]

@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) {

        // ...

(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:56 より引用)

この annotations パラメータに, @Functor 注釈が付いたクラスを表すオブジェクトの集合がやってきます. 後はそのオブジェクトを起点に, 抽象構文木 (abstract syntax tree, AST) を辿って型検査をすれば良いです. 基本的には Element#getEnclosingElement を使って AST を上に辿り, Element#getEnclosedElements を使って AST を下に辿ります. 文法要素の種類ごとに Element のサブインターフェースがあり, メソッドは ExecutableElement で表されます. Executable#getParameters メソッドで引数の要素が取れたりします.

AST のイメージは次の図のようになります. (細かく描くとまだありますが.)

ListFunctor<A>: TypeElement // クラス要素
`- map: ExecutableElement // メソッド要素
   `- <B>: List<? extends TypeParameterElement> // メソッドの型パラメータ要素
   `- morphism: List<? extends VariableElement> // メソッドのパラメータ要素
      ...

今回行いたいのは, 文法要素間の型についての整合性検査です. そこで Element#asType メソッドを使って, 要素から型を表す TypeMirror オブジェクトを取得します. この TypeMirror インターフェースにも, 要素とその型の種類によっていくつかのサブインターフェースがあります. 例えば ExecutableElement の型を表す ExecutableType があり, ExecutableType#getParameterTypes で引数の型が取れます. (この他にも便利な API がありますが, それはまた別の記事にします.)

さっきの AST の要素に対応する型は次の図のようになります. (こちらも細かいところは割愛してあります.)

ListFunctor<A>: DeclaredType // クラス型
`- map:  ExecutableType // メソッドの型
   `- <B>: List<? extends TypeVariable> // メソッドの型パラメータ
   `- morphism: List<? extends TypeMirror> // メソッドのパラメータの型
   `- ListFunctor<B>: TypeMirror // メソッドの返り値の型
      ...

全体をまとめると Element インターフェースを使い要素を辿っていき, 辿り着いた要素の TypeMirror を取得し型検査を行うのが, 今回の基本的な処理の流れになります.

試行錯誤 - Visitor

AST を辿る有名なパターンとして Visitor パターンがあります. Visitor パターンは JavaCC での AST の処理でも使われています. JSR 269 では Visitor の雛形となる抽象クラスも提供されており, 独自の処理が書きやすくなっています. [skrb_javase6_98]

しかし, 今回行う型検査は AST 上の複数のノード間の整合性の確認となるので, 基本的に一度に 1 箇所のノードしか扱えない Visitor パターンの恩恵はあまりありません. そういうわけで Visitor パターンでの処理の実装は破棄しました.

余談 - パターンマッチ

AST は様々な種類のノードで構成される木構造です. そのため, まず全ての種類のノードを表すインターフェースを用意し, 各ノードの種類ごとにサブインターフェースあるいは具象クラスを作成していくのが自然な設計でしょう.

そういった設計で組み上げられた AST という木構造の上を, ノードから下位ノードへ渡り歩きながら処理していくのが今回の実装となっています. そして, あるノードの下位ノードから目的のものを探し出すときには, まず型情報型情報を利用します. そこで, 型によるパターンマッチが欲しくなります!!

instanceof 演算子と Class#cast メソッドの組み合わせでも実質同じことができるのですが, 見た目やキャストの気持ち悪さから, できれば Java にも型によるパターンマッチが欲しいと感じました.

まとめ

函手を模した Functor 型クラスを Java で実現する手法として, 注釈と JSR 269 に基づいた注釈処理機を選択しました. これにより静的型検査の段階に独自処理を追加でき, より自然な型表現での Functor 型クラスを実現しました.

今後は,

  • ListFunctor の実装
  • 函手以外の型クラスの実装
  • 公理を検査するテストの作成
  • より高階なモナドトランスフォーマー用の注釈と注釈処理機の実装

などに挑戦していきたいです.

参考文献

[Kotlin_functor_ntaro]Ntaro. “Sample.kt.” N.p., n.d. Web. 5 Oct. 2015. <https://gist.github.com/ntaro/b20d303dd0da4916ccf9>.
[JSR269]“JSR 269: Pluggable Annotation Processing API.” The Java Community Process(SM) Program - JSRs: Java Specification Requests - Detail JSR# 269. N.p., n.d. Web. 5 Oct. 2015. <https://www.jcp.org/en/jsr/detail?id=269>.
[opengl-8080](1, 2) Opengl-8080. “Pluggable Annotation Processing API 使い方メモ.” Pluggable Annotation Processing API 使い方メモ. N.p., n.d. Web. 6 Oct. 2015. <http://qiita.com/opengl-8080/items/beda51fe4f23750c33e9>.
[skrb_javase6_95]櫻庭, 祐一. “「Java SE 6完全攻略」第95回 アノテーションを処理する その2.” Java技術最前線 - 「Java SE 6完全攻略」第95回 アノテーションを処理する その2:ITpro. N.p., 19 Jan. 2009. Web. 7 Oct. 2015. <http://itpro.nikkeibp.co.jp/article/COLUMN/20090115/322957/>.
[skrb_javase6_98]櫻庭, 祐一. “「Java SE 6完全攻略」第98回 アノテーションを処理する その5.” Java技術最前線 - 「Java SE 6完全攻略」第98回 アノテーションを処理する その5:ITpro. N.p., 09 Feb. 2009. Web. 7 Oct. 2015. <http://itpro.nikkeibp.co.jp/article/COLUMN/20090204/324186/>.