Multiple Dispatch in Modern JVM Languages

こんにちは、エンジニアの永井雅人 (@nagai_masato) です。G* advent calendar の 17日目を GREE engineers’ blog からお届けします。

dispatch ?

オブジェクト指向言語の文脈でのディスパッチとは、同じ名前のサブルーチンをパラメタの型で動的に判定する呼び分けを指します。ディスパッチには大きく分けて マルチプルとシングルがあります。マルチプルはメッセージの全てのパラメタを対象にしますが、シングルはそのうちレシーバのみを対象とします。この記事では Java や最近の JVM 言語での対応状況を交えてディスパッチについて話したいと思います。

Java

Java が対応しているのはシングルディスパッチです。イベントハンドラの例で説明します。今どきっぽくタッチイベントにしてみます。タッチイベント用のクラスのルートに TouchEvent があって、その下に具体的なイベントクラスが、そして TouchEventHandler というハンドラクラスがあるとします (実際の API にはインターフェースとか抽象クラスのような仮想的な上位階層があるかもしれませんが、それは置いておきます)。

class TouchEvent {}  

class TouchStartEvent extends TouchEvent {}  
class TouchMoveEvent extends TouchEvent {}  
class TouchEndEvent extends TouchEvent {}  
class TouchCancelEvent extends TouchEvent {}  

class TouchEventHandler {
    void handle(TouchEvent e);
} 

さて、ここで TouchStartEvent と他の TouchEvent で扱いを変えるハンドラを作りたいとします。派生型によるポリモーフィズムを期待するならたぶんこう書きます。

class MyTouchEventHandler extends TouchEventHandler { 
    public void handle(TouchEvent e) {
        System.out.println("... ?:(");
    }
    public void handle(TouchStartEvent e) {
        System.out.println("touch started :)");
    }
}

でもこれはうまくいきません。

TouchEventHandler h = new MyTouchEventHandler();
h.handle(new TouchStartEvent()); // prints "... ?:("

MyTouchEventHandler は見てくれますが、呼び出されるのは handle(TouchEvent) の方です。少し技術的な表現にすると、オブジェクトへメッセージを送る時、レシーバの型は動的に解決してくれますが他のパラメタの型は定義されたレシーバクラスのシグネチャに沿って静的に解決されてしまいます。これがシングルディスパッチです。コードへの期待と Java のコンパイラの見解との相違を解りやすくする為に次のようにしてみました。

class MyTouchEventHandler extends TouchEventHandler {
    @Override public void handle(TouchEvent e) { 
        System.out.println("... :("); 
    } 
    @Override public void handle(TouchStartEvent e) {
        System.out.println("It's my turn! :)");
    }
}

するとこう怒られます。

src/Overloading.java:19: error: method does not override or implement a method from a supertype
    @Override public void handle(TouchStartEvent e) {
    ^

Java のコード上で (つまりコンパイル時の仕掛けを用いずに)マルチプルディスパッチを模倣する伝統的な方法はありますが、それは次の Xtend の項に譲ります。

Xtend

Xtend は去年登場した比較的新しい JVM 言語 (比較的、と付けたのは今年も新しい JVM 言語が現れたから) です。Xtend もシングルディスパッチですが、”dispatch” キーワードを宣言してやればマルチプルディスパッチになります。

class MyTouchEventHandler extends TouchEventHandler {
    def dispatch void handle(TouchEvent e) {
        println("... :(")
    }
    def dispatch void handle(TouchStartEvent e) {
        println("touch started :)")
    }
}
val TouchEventHandler h = new MyTouchEventHandler()
h.handle(new TouchStartEvent()) // prints "touch started :)"

Xtend はクラスファイルではなく Java コードを生成するので、何をしているかはすぐにわかります。dispatch 宣言したメソッドを呼び出すプロキシ役のメソッドを前に置いています。これが先ほど触れた、Java でマルチプルディスパッチを模倣する方法です。

public class MyTouchEventHandler extends TouchEventHandler {
  protected void _handle(final TouchEvent e) {
    InputOutput.println("... :(");
  }
  
  protected void _handle(final TouchStartEvent e) {
    InputOutput.println("touch started :)");
  }
  
  public void handle(final TouchEvent e) {
    if (e instanceof TouchStartEvent) {
      _handle((TouchStartEvent)e);
      return;
    } else if (e != null) {
      _handle(e);
      return;
    } else {
      throw new IllegalArgumentException("Unhandled parameter types: " +
        Arrays.asList(e).toString());
    }
  }
}

アンダースコア1つだけというのは名前の衝突を気をつける必要があるのであまり上手くないと思いますが、それはさておいて Xtend がなぜでフォルトでシングルディスパッチなんだろうってことです。作者の1人の Sven Efftinge に聞いたら、既存の Java ライブラリとの間で問題が起きるのを避ける為、ということでした。シングルディスパッチ依存の酷いコードを気にして、新しいコードの書きやすさを損なうなんてもったいない気がしますが、僕がまだそれ程痛い目に合っていないだけかもしれません。でもとりあえず Xtend はマルチプルディスパッチに対応しています。

Groovy

Groovy はマルチプルディスパッチです。上に挙げた Java の例を Groovy で実行すると期待どおりに動きます。とても直感的。

TouchEventHandler h = new MyTouchEventHandler();
h.handle(new TouchStartEvent()); // prints "touch started :)"

パラメタの型が動的に判定され、handle(TouchStartEvent) が呼び出されます。もう少し踏み込んで説明すると、Groovy はコンパイル時にメソッドの呼び出しを呼び出し側のクラス、名前、そしてインデックスを保持する CallSite オブジェクトへと置き換えます。コードで表すとこんな感じです。

TouchEventHandler h = new MyTouchEventHandler();
CallSite handleN = // create a call site for the "n:handle"
handleN.call(h, new TouchStartEvent());

CallSite はレシーバが所有する handle メソッドのメタ情報を並べて、引数の数や型を見て段階的に目的のメソッドを探していきます。この動的な解決にはもちろん相応のコストがかかりますが、CallSite は一度呼び出しを行ったらメソッドをキャッシュするので2度目以降は即座にメソッドを呼び出します。最近の Groovy ではパフォーマンス上、どうしても動的さを捨てなければならない場合の為に @StaticCompile というアノテーションがあります。これを使った範囲は CallSite を使わない静的なメソッド呼び出しをするバイナリを吐くわけですが、そうなるともちろんマルチプルディスパッチできなくなります。

その他

他の JVM 言語についてはよくわからないので言及はできるだけ避けたいのですが、憂いを残さない為にもちょっとだけ調べてみました。間違っていたら指摘してください。Clojure には Xtend の dispatch に相当する “defmulti” というマクロがあるようです。Scala は対応していませんが、彼等は強力なパターンマッチングを盾にいらないと言うかもしれません。JRuby と Jython はそもそも本家の Ruby と Python が対応してないみたいです。Kotlin も気になるけれどもうお腹いっぱいなのでこの辺で。x使いの皆さんも自分の言語の振る舞いを見てみると良いかも知れません。

Author: nagai_masato