Servlet Garden

Java, Web Application and beyond

Flower

Archive for April, 2008

Duby: A Type-Inferred Ruby-Like JVM Language (Translation)

JRubyの開発者であるCharles O. Nutter氏が静的な型のあるRubyのサブセット言語Dubyを作りつつあるようです。Nutter氏のブログにDuby関連がいくつかあるので、勉強を兼ねて、順番に邦訳していこうと思います。今回、例によって勝手に翻訳したのはDuby: A Type-Inferred Ruby-Like JVM Languageです。

なお、DubyはJRubyのtrunk、つまり、svnからチェックアウトしたコードの中に作り込まれているので、試せるようです。

====================

その夜は決して忘れられないような、すばらしい夜になりました。

日曜日は一日中、まるでなにもうまく行かないような日でした。たぶん、土曜の夜にビールを何本も飲んで、赤ワインを何杯か飲んだ影響だと思います。原因は何でもいいのですが、もし私の思考が作業中にどこかにいってしまうようなことがあると問題を作り込んでしまうのではないかという恐れがあり、JRubyのバグを修正できるかどうか疑わしい状態でした。そこで、数ヶ月前に作ったバイトコードを生成するDSLをいじってみることにしました。

長い間ずっと、”Rubyのような”、おそらく、Rubyのサブセットのような言語で、いかにもJava言語らしいバイトコードに確実にすばやくコンパイルできるような言語がほしいと思っていました。Rubyのためのコンパイラではなく、また、サポートするのが難しくて、それ自信を実装するために使うには能率が悪いような言語にRubyをしたててしまう機能ががないようなものです。Javaのプラログラムをコンパイルしたときにできるような素直で、きっちりとした標準的なJVMのバイトコードを生成するような実際的なサブセット言語です。とはいっても、ほとんどRubyのような見かけと使い心地を維持していて、さらによいと思えるようなものです。

だから作ってみたのです!そして自分のバイトコードライブラリも使ってみました!

例えば、おなじみのフィボナッチ数を実装するとしましょう。

class Foo
  def fib(n)
    if (n < 2)
      n
    else
      fib(n - 2) + fib(n - 1)
    end
  end
end

これは通常のRubyのコードです。Fixnum型を入力に与えると、このメソッドは適当なフィボナッチ数を計算して値を返します。このメソッドの実行は次のような理由で、(J)Rubyでは遅いのです:

  1. JRubyではボックス化された整数値を使います。MatzのRubyやRubiniusはタグ付きの整数値を使ってパフォーマンスを向上していますが、私たちはJVMの機能に頼って、できるだけ最適化されるだろうことを期待しています(これは行き過ぎているくらいであることはわかっています)。それでもなお、プリミティブな型を直接使うよりも遅いのです。
  2. 比較演算や整数値の数学的な演算、フィボナッチ数の計算はすべてダイナミックなメソッド呼び出しです。このため、少なくても、メソッドを見つけだすコストが若干と、抽象化のコストがかなりかかります。これらを少なくすることは可能ですが、取り除くことはできません。
  3. Rubyにはたとえ使わなかったとしても、パフォーマンスを悪くしてしまうような機能がいくつもあります。例えば、局所スコープをいつでもとらえることができる状態のときに、局所変数を最適にストアするのはとても難しいことです。このため、トリックに頼るにしても、ヒープ上に局所変数をストアしてそれらを扱うにしても、処理は遅くなります。

静的な型言語を使っているときは、これら全てを取り去ることができます。例えば、Javaには、オブジェクト型とプリミティブ型の両方がありますが、プリミティブな型での演算は驚くほど速い上に、場合によってはJITがマシンコード相当のレベルに落としてくれます。また、フィーチャーセットがうまく限定され、現在のJVMで驚くほど高度なレベルにまで最適化できるようになっています。

しかし、もちろんJavaにも独自の問題があります。ひとつは、型をあまり憶測したり"推測"したりはしてくれないということです。これは、通常、プログラマがいたるところで型を宣言しなければならないことを意味しています。局所変数、メソッドの引数、戻り値すべてです。C# 3.0は型推論を随所に取り入れて、この問題をなくそうとしています。しかし、Cに似た言語を利用しようとすると { } や ; といった、コードを読みにくくするようなシンタックス関連で煩わしい問題にもなります。

上記のようなコードを書いた時に、型推論のロジックがあって、推論した型を動作の速い静的な型言語に返すことができたらいいと思いませんか?

class Foo
  def fib(n)
    {n => java.lang.Integer::TYPE, :return => java.lang.Integer::TYPE}
    if (n < 2)
      n
    else
      fib(n - 2) + fib(n - 1)
    end
  end
end

いいでしょう!これは、先ほどのものとまったく同じですが、今度はメソッドのボディ部分の直前のところが、ちょっとした型宣言のブロック(Rubyでいう hash/mapのような)で修飾されています。この型宣言は'n'という引数がプリミティブなint型で、メソッド自身がプリミティブなint型を返すことを表しています(これらは推測可能であることはわかっています。。。まもなくできるようになるはず)。型推論に基づいて選択されたプリミティブな操作を除けば、メソッドの残りの部分は期待通りに動いてくれるでしょう。確認のために、javapを使って逆アセンブルすると、コンパイラから以下のように出力されました:

Compiled from "superfib.rb"
public class Foo extends java.lang.Object{
public int fib(int);
  Code:
   0: iload_1
   1: ldc #10; //int 2
   3: if_icmpge 10
   6: iload_1
   7: goto 27
   10: aload_0
   11: iload_1
   12: ldc #10; //int 2
   14: isub
   15: invokevirtual #12; //Method fib:(I)I
   18: aload_0
   19: iload_1
   20: ldc #13; //int 1
   22: isub
   23: invokevirtual #12; //Method fib:(I)I
   26: iadd
   27: ireturn

public Foo();
  Code:
   0: aload_0
   1: invokespecial #15; //Method java/lang/Object."":()V
   4: return

}

追記:

  • Java言語と同様に、ディフォルトコンストラクタが生成されます。Rubyの"def initialize"コンストラクタも認識されるはずです。ただ、オーバロードを許すかどうかについてはまだ決めかねています。
  • フィボナッチ数の型シグニチャやそこから行われるメソッド呼び出しの型シグニチャ全てが正しい型を正しく推論していることに注目してください。
  • 全ての比較と算術演算が正しいバイトコードにコンパイルされている(iadd, isub, if_icmpgeなど)ことにも注目してください。

パフォーマンスは期待通りよくなっています:

$ jruby -rjava -e "t = Time.now; 5.times {Java::Foo.new.fib(35)}; p Time.now - t"
0.681
$ jruby -rnormalfib -e "t = Time.now; 5.times {Foo.new.fib(35)}; p Time.now - t"
27.851

以下にあげたのは文字列操作をおりまぜた例です:

class Foo
 def bar
   {:return => java.lang.String}

   'here'
 end
end

class Foo
 # reopening classes works in the same file only (for now)
 def baz(a)
   {a => java.lang.String}

   b = "foo"
   a = a + bar + b
   puts a
 end
end

もちろんこれは動いて、次のように出力されます:

$ jruby -rjava -e "Java::Foo.new.baz('Type inference is fun')"
Type inference is funherefoo

ここでも逆アセンブルした結果を表示しておきます:

Compiled from "stringthing.rb"
public class Foo extends java.lang.Object{
public java.lang.String bar();
 Code:
  0: ldc #13; //String here
  2: areturn

public void baz(java.lang.String);
 Code:
  0: ldc #15; //String foo
  2: astore_2
  3: aload_1
  4: checkcast #17; //class java/lang/String
  7: aload_0
  8: invokevirtual #19; //Method bar:()Ljava/lang/String;
  11: invokevirtual #23; //Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
  14: checkcast #17; //class java/lang/String
  17: aload_2
  18: invokevirtual #23; //Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
  21: astore_1
  22: getstatic #29; //Field java/lang/System.out:Ljava/io/PrintStream;
  25: aload_1
  26: invokevirtual #34; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
  29: return

public Foo();
 Code:
  0: aload_0
  1: invokespecial #36; //Method java/lang/Object."":()V
  4: return

}

ここで、+ 演算子が2つの文字列に対して動作していることが検出されていることに注目してください。数値演算ではなくString#concatが呼び出されるようにコンパイルされました。このたぐいのマッピングを追加するのは簡単です。型情報はどこにでもあるのでRubyのシンタックスをJavaの型にマップするうまいやり方を思いつくのは簡単なのです。

この言語の名前は現在のところDubyにしようと思ってます。"Doobie" のように発音しますが、これはDukeとRubyを足したものです。Duby!いい響きでしょう。変更することになるかもしれませんが、当面はこの名前をいくつもりです。

現時点では、Dubyはここで説明した機能があるだけです。今のところ、かなり限定されたRubyのサブセットです。例えば、Javaのすべてのプリミティブな型をサポートしてもいないので、やらなければならないことはたくさんあります。Dubyはstatic (class)なメソッドもサポートしていませんし、"initialize"メソッドも扱えるようになっていません。また、(名前空間のための)パッケージや(型名を縮めるために使う)インポート、スーパクラス、インタフェース、列挙型、ジェネリックスなど普段使っているものもサポートしていません。しかし、ここで説明したように、Dubyはわずかのコードに関しては機能的であることがわかるでしょう。ですから、10時間取り組んで作ったDubyはすばらしいスタートを切ったと思っています。他の機能は必要に応じて、時間がゆるせば、実装していけるでしょう。

私の計画についても触れておきましょう。かなりの人がRubiniusというRubyによるRubyの再実装が行っていることを知っているでしょう。Rubiniusは多数存在するSmalltalk VMのデザインを規範としています。さて、Ruby開発者がもっとJRubyに取り組みやすくするためにはDubyはおそらく最も良い方策ではないかと思います。DubyならRuby開発者はかなりRubyらしいコードを書けるようになでしょうし、本来のJava言語のスピードのパフォーマンスが出ることや、全てが正しく対応するようなコンパル時の型チェック機能を提供していることもわかるでしょう。つまり、一匹の亀だけにどうやってJVMのバイトコードを話して、作り出すかを教えて、"他の亀たちが全てをだめにする"ようなオーバヘッドを回避できます。

私はDubyのおかげで、JVM上にどのように言語を実装するかの新しいアイディアが多数出てくることも期待しています。とある言語の利用者にとっては、興味を持っているその言語を使って言語の実装’に貢献できるとしたら、それはなんといっても魅力的です。また、もし、コンパイラやDubyのようなサブ言語を思いつくことができたら、もっと幅広い開発者にとってJVMがさらに付き合いやすいものになるでしょう。

興味津々の人たちへ: DubyのコンパイラはほとんどRubyで記述されていますよ。