uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Staticalizer - G* Advent Calendar 2012-

f:id:uehaj:20121201155702j:plain

今年もやってまいりました恒例行事「G* Advent Calendar 2012」その4日目ということで、私が作ったプログラム「Staticalizer」を紹介します。発音は「スタティカライザー」で二〜三回舌を噛む感じで発音します。

これは、Groovy2.0で導入された静的Groovy機能をより便利につかうためのプログラムです。もとはJGGUG合宿で作りはじめたもので、品質はまだまだなのですが、この機会を借りまして公開します。

背景

Groovy 2.0で導入された静的Groovyはなかなか画期的なものだと思います。いくつかの利点がありますが、その一つは性能です。静的コンパイルを行うことで、動的型によるオーバーヘッドを回避でき、Javaに匹敵するほどの性能が理論的には期待できそうです。既存のGroovyコードを高速化してみたくなるかもしれません。

(参考: "Groovy 2.0の新機能")

それをやるのは、とっかかりとしては@CompileStaticや@TypeCheckedといったアノテーションを付けるだけなのですが、型情報が不足している場合などには怒られてしまいます。

例えば、

import groovy.transform.*

@CompileStatic
def greet(a) {
  println "Hello "+a.toUpperCase()
}

greet("world")

は以下のようなエラーです。

org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
/tmp/work/hello.groovy: 5: [Static type checking] - Cannot find matching method java.lang.Object#toUpperCase(). Please check if the declared type is right and if the method exists.
 @ line 5, column 20.
     println "Hello "+a.toUpperCase()
                      ^

引数aの型が判らないために、「toUpperCase()」というメソッド呼び出しが正当かどうかコンパイル時に決定できないためです。このようなエラーを修正するためには元のコードを

import groovy.transform.*

@CompileStatic
def greet(String a) { // ← aの型を指定
  println "Hello "+a.toUpperCase()
}

greet("world")

のように修正する必要があります。

型推論と解決すべき課題

静的Groovyでは「型推論」の機構が導入されており、場合によっては型を省略したままにしておくこともできますが、このようにメソッドを呼び出しをまたがると推論(「文字列定数"world"を渡しているのだから、aはStringだろう」)はできず、型を省略することができません。推論ができない理由の一つは、メソッドは複数箇所から呼び出される可能性があり、それぞれの呼び出しで異なる型の引数が与えられるかもしれないからです。例えば、greet(3), greet("abc")と別の箇所で呼ばれているかもしれません。もしそうなら、引数の型はすべての呼び出しで与えられる型の「LUB(Lowest Upper Bound」と呼ばれる型になるべきで、これは「共通の親クラスのうち一番下位にある型」です。例えば、3と"abc"のLUBはjava.lang.Objectですが、intとdoubleのLUBはjava.lang.Number型になります。

このようなLUBを調査するのはたいへんな手間になります。たとえば、複数のinterfaceをimplementsしている場合などはDAGの探索になってきます。

また、ある値が、呼び出したメソッドの戻り値で、その値を決定しているのはそのまた呼び出し先で…というような呼び出しが連鎖しているときも、悪夢となります。

大規模なdynamic groovyコードを後から静的にするのは、一般にはとても・非常に大変です*1

とはいえ、メソッド引数や返り値の型だけでも指定しておくことで、型推論が有効に動作する可能性が高くなる、とも言えます。

Staticalizerの登場

そのような状況で、今回作ったStaticalizerが役にたつはずです。staticalizerはGroovyプログラムを実行させて、実際の型(メソッド・コンストラクタ・クロージャの引数の型、メソッド戻り値の型)を収集記録し、その情報をもとにソースを書き換えることを支援してくれます。LUBの計算も自動的にやってくれます。

インストールと設定

staticalizer-0.1-bin.jarをこちらのbinの方からダウンロードし、どこか適当なところ(以降、STATICALIZER_HOMEと呼ぶ)に展開します。

そして、STATICALIZER_HOME/binをPATHに通します。

使い方

staticalizerの使い方には、コマンドラインから実行する方法と、AST変換のアノテーション(WithTypeLogging)を使う方法の2種類があります。まずは簡単なコマンドラインから実行する方法から。まず、

def foo(n) {
  Closure c = {a,b -> a+b}
  return c(n, n)
}
println foo(3)
println foo(3.5)

こんなGroovyコード(hello.groovy)があるとします。このとき、以下のようにstaticalizerコマンドを実行します。staticalizerコマンドは、引数の指定方法はgroovyコマンドと同じであり、groovyコマンドで実行できる任意のスクリプトを対象にして同様に実行できます。

$ staticallizer hello.groovy
6
7.0
Patch file 'staticalizer.patch' generated. Apply the patch with command line:
 % (cd /; patch -b -p0) < staticalizer.patch

この結果、カレントディレクトリにstaticalizer.patchというパッチファイルが生成されます。
この段階ではスクリプトのソースコードなどは変更されていません。パッチファイルの中身を確認して、

% cat staticalizer.patch
--- /work/hello.groovy 2012-01-04 12:01:17.000000000 +0900
+++ /work/hello.groovy.changed 2012-01-04 12:01:17.000000000 +0900
@@ -1,0 +1,1 @@
+// TODO: Change method argument type: foo(java.lang.Number n)
@@ -1,0 +2,1 @@
+// TODO: Change return type: java.lang.Number foo(...)
@@ -2,0 +4,1 @@
+// TODO: Change closure argument type: { java.lang.Number a,java.lang.Number b -> .. }


問題なければ以下のようにパッチコマンドを実行します*2

$ (cd /; patch -b -p0) < staticalizer.patch
patching file /work/hello.groovy

するとhello.groovyは以下のように修正されます。(元ファイルは拡張子が.origに変更されて同じディレクトリに保存されています。)

def foo(n) {
// TODO: Change method argument type: foo(java.lang.Number n)
// TODO: Change return type: java.lang.Number foo(...)
  Closure c = {a,b -> a+b}
// TODO: Change closure argument type: { java.lang.Number a,java.lang.Number b -> .. }
  return c(n, n)
}
println foo(3)
println foo(3.5)

このコメントをつらつらと参考にしながら、メソッドの宣言を修正します。また、本来の目的だったCompileStaticなどのアノテーションも付与していきます。ここは手動なのです。

import groovy.transform.CompileStatic
@CompileStatic
Number foo(Number n) {
  Closure c = {Number a,Number b -> a+b}
  return c(n, n)
}
println foo(3)
println foo(3.5)

完成!*3

アノテーションを使用する方法

staticalizerスクリプトを使用する代りに、静的化したいメソッド(もしくはクラス)に@WithTypeLoggingというアノテーションを付与することもできます。

import org.jggug.kobo.staticalizer.transform.WithTypeLogging
class X {
  @WithTypeLogging
  int foo(i) {
    return 0
  }
}

これをstaticalizerのjar(STATICALIER_HOME/lib/staticalizer-0.1.jar)をクラスパスに通した上で実行します(そのうちgrabで取れるようにしたいですね)。そのあとstaticalizer.patchが生成されるのはstaticalizerコマンドを使った場合と同様です。

なお、staticalizerコマンドは内部的処理には指定したスクリプトをこのアノテーション(AST変換)を適用して実行しているだけです。

アノテーションを使う場合、全メソッド・全クラスではなく、必要なメソッドやクラスに対して選択的に指定することができます。また、IDEで使用する場合など、都合が良い場合があると思います。

免責

突貫で作ったので、まだまだよほど品質が低いです。githubで公開してますのでissueで報告してくだしあ。

最後に

Toby55@新潟 さん、次お願い致します!

*1:最初から静的と決めて開発すればこの限りではない。

*2:Windowsで適用するには、cygwinなどのpatchコマンドもしくはunified diff形式を処理できるパッチ適用可能なツールが必要です。Eclipseでできるっけか?

*3:ちなみに、型情報の指定だけではすまないケースも多々あるので、これで静的化の作業がすべて済むわけではありません。