JavaでFreeモナドを表現するためのテクニックやexistential type(存在型)の話

久しぶりにJavaで遊んで、JavaでFree Monad書いた際に得られた知見を記しておきます。

https://github.com/xuwei-k/free-monad-java

筆者は、Javaの言語仕様や、型理論に関して、人並みかもしれないけど(?)別に専門家ではないので、あまり厳密なこと求められても困るというか、ちょっと変なこといってる可能性もありますが、そこは生暖かい目で見守ってください。案の定指摘をもらったので、コメント欄もちゃんと読むといいよ!




さて、突然ですが、例えばこれ

http://stackoverflow.com/q/1031042/605582

を見ると
「本質的には、Javaのwildcardと、Scalaのexistential typeは同じ」
的なこと言われてます(?)が、Scalaの抽象型を使用すると可能な以下の様なことが、Javaのwildcard使っても不可能でコンパイルエラーです・・・*1

public class Main {
  public static void main(String[] args) {
    Foo<?> a = new Foo<>();
    a.foo(a.value());
  }
}

final class Foo<A>{
  public A value(){
    // コンパイル可能かどうか?のみが重要で実装はどうでもいいのでnull返すよ
    return null;
  }

  public void foo(A f){
  }
}
object Main {
  def main(args: Array[String]){
    val a = new Foo()
    a.foo(a.value)
  }
}

class Foo{
  type A
  def value: A = {
    null.asInstanceOf[A]
  }
  def foo(f: A): Unit = {
  }
}

まぁなのでwildcardは今回やりたかったことには使えないようです。上記のJavaコード、コンパイル通ってくれてもいい気がするけど、Javaさんはそういう仕様なんですかね?(これ以上自分で仕様書とか読んで調べる気は起きないので、誰か詳しい人いたら教えて欲しい)


さて「今回やりたかったこと」を説明してなかったですね。FreeのGosubというデータコンストラクタを扱う際に、上記のようなことをやってexistential type(存在型)*2を使えると型安全になって便利なのです。
(ところで、存在型がなんなのか?とか、存在型の定義とか、この場合のことを本当に存在型と言っていいのか?というのがわかっていないくらいには、色々自分の知識はあやしいです)



FreeのGosubは、スタックあふれないようにするためのある意味苦肉の策(?)で、HaskellのFreeモナドでは必要ないらしいし、Scala独特の難しい部分です。詳しく知りたい人はこれ
http://days2012.scala-lang.org/sites/days2012/files/bjarnason_trampolines.pdf
とか読んでください。


さて、実はScalazでも型安全あきらめて、FreeのresumeやflatMap内で、Any型を使っていたんですが
https://github.com/scalaz/scalaz/blob/9315491ba7454050058492c2608da43839ab52f4/core/src/main/scala/scalaz/Free.scala#L64-L65
つい先程、abstract type member使って型安全にしたものをpull reqしてmergeされました。
https://github.com/scalaz/scalaz/pull/730


これと同じことをJavaでやりたいわけです。そもそもJavaでは、

  • 末尾再帰最適化がなかったり
  • Scalaより型を明示しないといけない箇所が多かったり
  • 型によるパターンマッチがないので、明示的なキャスト発生したり

色々面倒です。で、Free Monadを移植するにあたって、Free#resumeを書くところが一番面倒で、もう型がわけわからなくて死にそうになりました。
Scalazでの現行versionがAny型を使っているのと同じように、Gosubの型パラメータにはjava.lang.Object型を当てはめておけば、(C#やF#はどうなのか知らないが)Javaではtype erasureのお陰で、べつに普通に動きます。
しかし、java.lang.Objectを明示的に使うのは気持ち悪い*3わけです。かといって、この場合wildcard使っても解決しません。

というわけで、結果的に考えついたのが、以下のようになりました。

https://github.com/xuwei-k/free-monad-java/blob/eab1b5d65bcc1d3a085f489dffb53aa0cb1f9be4/src/main/java/free/Free.java

  private static <X1, X2, F, A> Either<_1<F, Free<F, A>>, A> resume(Free<F, A> current, final Functor<F> F) {
    while(true) {
      if(current instanceof Done){
        return Either.right(current.asDone().a);
      }else if(current instanceof Suspend){
        return Either.left(current.asSuspend().a);
      }else {
        final Gosub<F, X1, A> gosub1 = current.asGosub();
        if(gosub1.a instanceof Done){
          current = gosub1.f.apply(gosub1.a.asDone().a);
        }else if(gosub1.a instanceof Suspend){
          return Either.left(F.map(o -> o.flatMap(gosub1.f), gosub1.a.asSuspend().a));
        }else {
          final Gosub<F, X2, X1> gosub2 = gosub1.a.asGosub();
          current = gosub2.a.flatMap(o ->
            gosub2.f.apply(o).flatMap(gosub1.f)
          );
        }
      }
    }
  }

X1, X2というのが、そのための型です。
X1とX2は、メソッドの型パラメータには現れているけど、それをメソッドのパラメータでは全く使っていません。また、メソッドの戻り値型にもX1, X2は現れません。
内部でGosubを束縛するところで Gosub gosub1 というように使っています。

自分で思いついていおいて、なかなか面白いなーと思ったんですが、もっといい方法とかあったら教えて下さい。あと、このテクニックがすでにどこかで紹介されてたり使われてたりしたら教えて下さい。

あと、この「存在型のためScalaの抽象型相当のための型パラメータが無駄に増えたメソッド」をpublicにしてしまうと、謎の「内部実装としてしかつかってない型パラメータ」が露出してしまってあまり良くないと思ったので、privateなstaticメソッドにきりだして、インスタンスメソッドのresumeは、staticメソッドを呼ぶようにしました。


ちなみに、resumeのメソッド内で、(whileを使わずに再帰していいなら)きしださんが、このblog

Java8には型推論があるので型指定不要で変数が使えますよ

で紹介してるように、

public static<T> void let(T value, Consumer<T> cons){
    cons.accept(value);
}

というような便利関数作っておけば、もっとローカル変数の型(というかローカル変数そのもの)は省略できます。しかし、ScalaJavaにおけるFreeモナドのGosubやresumeは、スタックをあふれさせないための工夫なので、せっかくGosubのデータコンストラクタ作っても、resumeでスタックをあふれさせては意味がありません。なので、(末尾再帰最適化がないJavaでは)しょうがなくwhileループにしています。

*1:この場合は、Scalaのabstract type memberではなく、forSomeのことだろうか・・・?

*2:この場合は存在型じゃないらしい?コメント参照

*3:というか型安全でなくなる