オフラインどう書く第九回の問題をJava 8 Lambdaでやってみた。

という記事を書きましたが、今回はJava 8 Lambda式を使って書きなおしてみました。

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

class BusJava8 {
  
  int ceil10(int n) { return (int)(Math.ceil(n/10) * 10); }
  
  int half(int n) { return ceil10(n/2); }
  
  List parse(String input) {
    String[] tmp = input.split(":", 0);
    String p = tmp[0];
    String list = tmp[1];
    return Arrays.asList(Integer.parseInt(p), Arrays.asList(list.split(",", 0)));
  }
  
  int calc(int basePrice, List<String> passengers) {
    Map<String,Function<Integer,Integer>> map = new HashMap<>();
    map.put("An", (Integer it)->it);
    map.put("Ap", (Integer it)->0);
    map.put("Aw", (Integer it)->half(map.get("An").apply(it)));
    map.put("Cn", (Integer it)->half(map.get("An").apply(it)));
    map.put("Cp", (Integer it)->0);
    map.put("Cw", (Integer it)->half(map.get("Cn").apply(it)));
    map.put("In", (Integer it)->half(map.get("An").apply(it)));
    map.put("Ip", (Integer it)->0);
    map.put("Iw", (Integer it)->half(map.get("In").apply(it)));
    Map<String,List<List>> groups =passengers.stream()
      .map(it -> Arrays.asList(it, map.get(it).apply(basePrice)))
      .collect(Collectors.groupingBy(it -> (((String)it.get(0)).substring(0,1))));
    long size = (long)(groups.get("I").size()-groups.get("A").size()*2);
    groups.get("I").sort((a, b) -> (Integer)(a.get(1)) - (Integer)(b.get(1)));
    List<List> tmp = new ArrayList(groups.get("A"));
    tmp.addAll(groups.get("C"));
    tmp.addAll(groups.get("I").stream().limit(size).collect(Collectors.toList()));
    return tmp.stream().mapToInt(it->(Integer)it.get(1)).sum();
  }
  
  static void test(String input, String answer) {
    Java8 b = new Java8();
    List tmp = b.parse(input);
    int basePrice = (int)tmp.get(0);
    List<String> passengers = (List<String>)tmp.get(1);
    int expected = Integer.parseInt(answer);
    int result = b.calc(basePrice, passengers);
    assert result == expected;
  }

  public static void main(String[] args) {
      test("1480:In,An,In,In,In,Iw,Cp,Cw,In,Aw,In,In,Iw,Cn,Aw,Iw", "5920");
  }
}

上では、自分がJava8に全く不慣れなため、非効率・冗長な記述をしているところがあるかもしれません。また、そもそも元のクロージャを駆使したアルゴリズムがGroovy向きだった、など比較としてはフェアではないかもしれませんので、ご留意を。Java 8Lambdaの仕様も未確定だと思いますので、現時点でのeaでの仕様であることも注意ください。

で、感想を言うと、こりゃ書くのたいへんだわ、です。Java8の補完対応IDEを使わなかったこと、などもあると思いますが…。あと結構エラーメッセージが怖いです。たとえば

Java8.java:28: エラー: 不適合な型: 推論変数Rには、不適合な境界があります .collect(Collectors.groupingBy(it -> (((String)(it.get(0))).substring(0,1))));
^
等価制約: Map>>
上限: Map,Object
R,Tが型変数の場合:
メソッド collect(Collector)で宣言されているRはObjectを拡張します
インタフェース Streamで宣言されているTはObjectを拡張します
INT#1,INT#2がintersection型の場合:
INT#1はObject,Serializable,Comparableを拡張します
INT#2はObject,Serializable,Comparableを拡張します

とか。型指定を1つ間違えただけで上のメッセージは…。

他に、Groovyと比べると、例えLambdaを使っても、

  • オペレータオーバーロードが無い
  • Collectionとstreamが別ものである
  • リストやマップリテラルが無い
  • 破壊的ではないリストの結合とかsortとかが無い(見付けられなかっただけかも…)ようなので、メソッドチェインが途切れてしまうのも気になります

などにより、冗長性は高くなるようです。シンプルなケースを除き、高階関数をバリバリ使って関数型ライクに書くのは余り積極的になれない気がしました。リリースまでには多くが解決されてしまうかもしれませんが*1

Lambda式の良い点は、GroovyのCompileStticに比べても、型推論の精度が高いということ。GroovyのCompileStaticでは、クロージャの戻り値の型は推論に寄与してないようで、チェインすると2個目以降に明示指定が必要になります。

さて、速度はどうだったか、などはJJUG CCC 2013 Springのセッションにて(ステマ)。

*1:まあ、lambdaの主眼はマルチコア細粒度並列処理と言われているので、もしそうなら通常の処理にあえて使う必然性もないわけですけれども。

てっとり早くGroovyでJava8のLambdaを使う

Groovy-MLで議論も始まっており、Groovyは将来的にはJava8正式対応するかと思いますが、世の中ではJava8アーリーアクセス版とかで話題になってるので、試してみました。

シンタックスのレベルでは、当然GroovyはLambda式を扱えませんが(決まってないし!)、Lambda式の実装がSAM型のサブクラスのインスタンスであることを踏まえると、以下のように例えばExpandoMetaClassでアダプタを定義してやれば、クロージャでJava8のStreamを叩けるわけです。

import java.util.function.*
import java.util.stream.Stream

class StreamTest {
  static test01() {
    List<String> names = ["hoge hoge", "foo bar", "junji", "uehara"]
    names.forEach{ println it }
  }
  
  static test02() {
    List<String> names = ["hoge hoge", "foo bar", "junji", "uehara"]
    names.stream()
      .filter { it.length() > 5}
      .map {"[" + it + "]"}
      .forEach{ println it }
  }
  
  static main(args) {
    Iterable.metaClass.forEach = { Closure clos -> delegate.forEach(new Consumer(){ void accept(arg){ clos.call(arg) }}) }
    Stream.metaClass.forEach =  { Closure clos -> delegate.forEach(new Consumer(){ void accept(arg){ clos.call(arg) }}) }
    Stream.metaClass.filter = { Closure clos -> delegate.filter(new Predicate(){ boolean test(arg){ clos.call(arg) }}) }
    Stream.metaClass.map = { Closure clos -> delegate.map(new Function(){ Object apply(arg){ clos.call(arg) }}) }
    test01()
    test02()
  }
}

もちろんこんなことしなくても上のレベルはGroovyだけでできるし、無限長コレクションを使いたければGroovy Stream(github)というのもあるんだけど、上はあくまでJdkAPIのStreamをGroovyのクロージャで利用している、というところがポイントです。

先のMLでの議論にあるように、クロージャとLambda式では得失がある*1ので、将来のGroovyにおいて、クロージャとLambda式が併存するのか、クロージャに一本化するべきなのか、その逆か、実装は、などはこれからの議論ということになるのでしょう。

使用したJava8のバージョンは

java full version "1.8.0-ea-lambda-nightly-h4200-20130429-b88-b00"

です。

参考:

*1:例えばGroovyのクロージャにおけるローカル変数のキャプチャは実効finalとかではなくリファレンスを経由してアクセスするので、値を変更できる代りにローカル変数アクセス時のオーバーヘッドがある

JavaOne 2012 Tokyo/JVM言語BOF

JavaOne Tokyoに参加しています。
表記のBOFにはスピーカーの一人として参加致しました。資料はこちら。

直前の暴風雨で、予定していた懇親会が流れたせいで(?)、慣れ合い/筋書き/打ち合せなしのガチンコ勝負へ…。

@yuyoroさんの究極の秘密兵器とその崩壊、唖然とさせるダッシュボード、「Javaは○○だし」暴言などなど、個人的にも最も楽しかったセッションでした。

客観的に見ると、マイナー言語同士で争ってどうすんだ、って話ではありますが、id:nobeansさんの奮闘の甲斐あり、Groovyがめでたく1位に(ぱちぱち)。でも、さすがの@nahiさんによるJRuby with indyの性能は素晴しい。Scalaのパターンマッチうらやましい。@tmizuさんの冷静なつっこみ、id:kiy0taka さんの司会もすばらしす。奥さんの投票アプリ*1はGroovyFXというのも加点ポイントだったか。LTでは明らかに理解している人が僅少であろう型プログラミングの話題や、Jrubyのコミッターはいい人(これが唯一の和む話題)とか、Oracleの牙城でうっかり禁句のAndroidのgradle試験の話題をだす@kyon_mmさん、などみんな楽しい。これがBOFの醍醐味かと。@toby55kijさんの的確な時間管理のおかげで流れも良く終りました。みなさん、おつかれさまでしたありがとう。
次回は完膚なきまでに打ちのめしますのでまたやりましょうw
だれかTogetterでまとめてくれないかのう。

(追記)
id:orangecloverさんがまとめてくれました。たいへんありがとうございました。
http://togetter.com/li/283845

*1:この堅牢たる高可用性・ハイスケーラブルアプリの裏話はLTで明らかになりましたが、愕然。

GVM: GroovyでJavaVMを書いたよ

前置き

さてやっと発売日となりました、私も著者の一人として執筆に参加している「プログラミングGroovy」ですが、ご覧になった方もいらっしゃると思います。

プログラミングGROOVY
プログラミングGROOVY
posted with amazlet at 11.07.06
関谷 和愛 上原 潤二 須江 信洋 中野 靖治
技術評論社
売り上げランキング: 4587

興味がありましたら読んで見てください。Groovy普及の1助になれば幸いです。Javaプログラマであれば読んで損はない本だと思います。

ghello

さて、発売日といえば、昨日は著者の1人である関谷さん(id:ksky)が、#ghelloというイベント(まとめサイトGoogleキャッシュ)を行っていて、これはみんなでGroovyでいろいろな「Hello World表示プログラム」を書いて、Twitterで#ghelloタグつけて発言する、というもので、なかなか盛り上りました。私も及ばずながら2、3個ツィートして見ました。2番目の「ぉうききぐじぐこきぅ」というのがちょっと狂気入ってそうな感じでお気に入りです。



良い企画だったと思います。ただ、Twitterなので140文字(しかもハッシュタグと次の人指名含めて)制限があり、これが予想外に厳しくて、ストレスが溜る溜る。

GVM: JVM Written in Groovy

なので、カッとなって、文字数制限なしで作ってみました*1。方針としてはGroovyでJava VMを書いてその上でHello Worldを表示させるJavaコード(コンパイルしたクラスファイル)を動作させる。

コードはgistに公開しましたのでリンクしておきます。本体は長いのでこのエントリには末尾に貼っておきます。

以下は実行例です。

$ cat Hello.java
package sample;
public class Hello {
    public static void main(String[] args){
        Hello h = new Hello();
        h.hello();
        System.out.println(h.plus(1,2));
        for (int i=0; i<10; i++) {
            System.out.print("i=");
            System.out.println(i);
        }
    }
    void hello() {
        System.out.println("Hello World");
    }
    int plus(int i, int j) {
        return i+j;
    }
}
$ mkdir classes
$ javac -d ./classes Hello.java
$ groovy ./gvm.groovy -cp classes sample.Hello
Hello World
3
i=0
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9

$ groovy ./gvm.groovy -h
usage: groovy gvm.groovy <class FQCN>
 -cp,--classpath <arg>   classpath
 -demo,--demo            demo
 -h,--help               usage
 -v,--verbose            verbose

デモモード(-demo)もあるので、Javaソースを作ったりjavacしなくてもgvm.groovy単体で実行することができます。

クラスファイルの読み込みとクラス構造やバイトコードの保持については、asmにすべて任せて*2さぼっています。asmはGroovyに組みこまれているバイトコード操作用のライブラリです。Groovy自体がasmによるバイトコード生成に依存して動作しています。

機能的には、足し算はできるが掛け算は(サンプルコードに含まれてないので)実装してないとか、読み込めるクラスは1つでディレクトリのみからでjarは対象外とか、そんな極小な感じです。

Java VMは前から一度は実装してみたいなと思ってて、Groovyなら短時間でできるだろうと思ってやってみました。確かにわりと簡単にはできたのですが*3、適当に作りすぎたのであんまり勉強にはならなったorz。

工夫としては、asmのバイトコード命令をあらわすクラス群(*InsnNode)にmetaClassで「.eval()」メソッドを注入しているところで、これによってコード量が劇的に削減され、単純化されてるんじゃないかと思います。一般に、JavaのクラスライブラリやAPIと連携したりカスタマイズしたりして動くコードを書くのは、Groovyの右に出るものは無いわけです。

Groovyの書き易さは、あらためて本当に体感しました。いやこれはほんとに凄い技術です。Javaで書く気には到底ならないなあ。

Groovy Rocks!

gvm.groovyコード

*1:というのはウソで、半分ぐらい正月につくって放置してましたのを一区切りつけただけです。

*2:asmだとクラスファイルフォーマットの汚いところ(コンスタントプールでlong値は2エントリ取る)とか見えなくなるので逆に味けない。

*3:開発工数は子育てしながらで合計3〜4日ぐらい。

速いぞGroovy!

Javaと同等レベルに高速に実行できるGroovyコードを書く方法が紹介されてます。
Yes, Fibonacci in Groovy can be as fast as Java !
こんなスクリプトです。

@ast.Bytecode
int fib(int i) {
 l0
    iload 1
    iconst_2
    if_icmpge l1
    iconst_1
    _goto l2
 l1
    frame SAME
    aload 0
    iload 1
    iconst_2
    isub
    invokevirtual '.fib','(I)I'
    aload 0
    iload 1
    iconst_1
    isub
    invokevirtual '.fib', '(I)I'
    iadd
 l2
   frame same1,'I'
    ireturn
}

int groovyFib(int i) { i<2?1:groovyFib(i-2)+groovyFib(i-1)}
println "Pure Groovy"
long sd = System.currentTimeMillis()
println groovyFib(40)
println "Computed in ${(System.currentTimeMillis()-sd)}ms"

println "Bytecode Groovy"
sd = System.currentTimeMillis()
println fib(40)
println "Computed in ${(System.currentTimeMillis()-sd)}ms"

あがが。
あごがはずれるわい。
Groovyで表現されるJava Bytecode DSLですね。
AST変換でバイトコード生成し、実行します。

余談ですが、Groovy 1.8では、プリミティブの演算が相当速くなるようですね〜。たのしみ。

HtmlUnit賛歌

知っている人には、今さらですが、HtmlUnitというものがあります。名前からするとxUnit系の、ユニットテストツールのような気がするかもしれませんが、違うよ!全然違います。誤解を招く名称です*1

これはGUIレスなブラウザなのです。GUIがないだけでなく、CUIコマンドラインインターフェースもありません。唯一できるのは、プログラムからブラウザの機能をJavaAPIを通じてJava(やGroovy)から呼び出し、そのAPIメソッド呼出しの結果を得るだけです。「非対話型ブラウザ」です。

いままで、XmlParserやNekoHtmlやHttpBuilderといったもののみでスクレイピングをしてきた諸兄は、HtmlUnitを一回使ってみるのがよろしいと思いました*2HtmlUnitを使うことで、ログイン処理や、ページ遷移が簡単になったり、ページの表示にJavaScriptとかを使うケースも対応できたりとか、それらを使ってちまちまやるよりも圧倒的に簡単になることがあります。プロトコルレベルでやるってことは、基盤となるブラウザ機能の必要なところは自前で作らないといけないってことですから。ブラウザの実装であるHtmlUnitは、そこらへんJavaScriptを含めて実装済みになるので、楽なのです。

FireFoxをお使いなら、こちらHtmlUnitScripterHtmlUnitのコード断片を生成させるのも良しでしょう。

HtmlUnitでは、XPathを使う*3ことができるのですが、これも気に入りました。XPathはそれ自体汎用的で強力であるだけでなく、ブラウザと連携する他のツール(AutoPager Add-on(Chrome用もある)とかWebTest Recorder Sidebarでも使える)もあるのが強みです。

そんなこんなしてHtmlUnitを使って、

などを、JGGUG合宿の企画g100ponのリモート参加ネタとして上げさせていただきました。サンプルとして御笑覧下さい。なおリモート参加にあたり、規定コメントの不足やtwtterの送信を怠るなど、各位には失礼をば致しました。

*1:もちろんWeb機能テストで使えますが、そんなこと言ったら足し算だってユニットテストで使えます。

*2:HtmlUnitは下位層ではNekoHtmlを呼び出しています。

*3:HtmlPage#getByXPath()

スプーンとAST変換

さっき書いたenumの記事で思い出したのでご紹介ですが、フランスのINRIAがやってる(やってた?)Spoonというプロジェクトがありまして、これはなにかというと、Javaコンパイラを拡張して、特定アノテーションの指定をきっかけとしてコンパイラのコード生成とかに介入し、デザインパターンの実現をはじめとして、さまざまに機能拡張できるというものです。

これはなかなかGroovyのAST変換に近しいものです。Java版のAST変換というべきか、時期的に言ったらSpoonの方が遥かに先ですから、SpoonをGroovy流にしたのがAST変換というべきか。

Spoonを使ったプログラミングの例についてはこちらにありますが、さっきのN-tonだとか、分散計算だとかが実現されてます。DelegateGroovyに同名のがありやすね。使い方はちょっとちがうみたいですが。

実用性が高そうなものとしてはVisitorというのもあって、Visitorパターンで必要なさまざまなハウスキーピングメソッドを生成してくれるらしいのですが、GroovyのAST変換でも欲しい気がします。