uehaj's blog

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

Java VM上の言語の覇者を決める(但しJava以外)スクリプトボウルの履歴

米国開催のJavaOneカンファレンスで恒例の人気セッションとなっている「スクリプト・ボウル」というのがあります。Java VM上で動作する言語同士のガチ勝負で、コード例とか、技術力とかいろいろな側面からパネルセッションでバトルして、その年勝者を決める、というものです。

自分では残念ながら直接見たことはないので想像ですが、「朝まで生テレビ」で言語バトルするようなものです。違うか。

エンターテイメント要素も多々あるでしょうし、人気とか「情熱」みたいなのも評価されるかもしれないし、コミュニティの「動員力」とかの要素もひょっとしたらあるかもしれませんね。そういうものも含めて、おもしろい勝負なのではないかと思います。もちろん技術者同士でやってるので、技術的なおもしろみとかもあるでしょう。プレゼンのうまさとかもあるでしょう。

んでその過去の結果をまとめてみました。

候補結果
2008Scala,Groovy,JRuby,Jython JRuby優勝 リンク
2009Scala,Groovy,JRuby,Jython,Clojure Groovy優勝 リンク
2010Scala,Groovy,JRuby,Clojure Groovy優勝 リンク
2011Scala,Groovy,JRuby ScalaとGroovyが同位優勝 リンク*1
2012Scala,Groovy,JRuby,Clojure Scalaが優勝 リンク
2013Scala,Groovy,Nashorn,Clojure Groovyが優勝 リンク

圧倒的じゃないか!!
とか言ってみる。
少なくとも皆勤賞ではある。

*1:リンク先を見るとScalaの人は相当納得してない。

プログラミング言語Frege(フレーゲ)を紹介します

これはマイナー言語 Advent Calendar 2013の21日目の記事です。
Frege(フレーゲ*1 )を紹介します。

Fregeは、Java VM上で動作するHaskell風の言語です。以下のような特徴を持っています。

これらの特徴は、Haskellと共通するものであり、構文も基本的なところについてはHaskellとだいたい同じか似ているかもしくはサブセットです。標準関数やデータ型やモジュールについても、Haskell 2010からたくさん引っぱってきているそうです。

しかしながら、Fregeはその目標において、Haskellとの完全な互換性を達成しようとはしていません。実際かなり違っています。特にJava VM上で有用であることに重点が置かれており、プリミティブ型はJavaのものと等価なものを使用しており、Javaで定義されたメソッドを呼び出すためのインターフェースが工夫されています。

また、Haskellですっきりしてないところなどを直したり、改良しようとしているところもあります(例: MonadはApplicative型クラスのインスタンスとかデータ型がスコープを持つとか。)。またHakell標準ではない機能(GHC拡張)を標準的に使えるようにしているところもあります(例えばランクn多相,RankNTypes)。

Fregeの面白いところの一つは、fregeのコンパイラは中間コードとしてJavaソースコードを吐くところです。コンパイル結果としてJavaソースも静的ファイルとして残るので、遅延評価っていったいどういうことだろう?など、Javaコードを良く読むとわかるかもしれません。REPLではオンメンモリでjavacを走らせてるみたいです。

fregeの使い方紹介

オンラインREPL

お気軽に試すにはオンラインのREPLがありますのでどうぞ。
「:java」で生成されたJavaソースを見ることができます。

REPLをインストールして動かす

frege-replのページからたどれるダウンロードページからfrege-repl-x.x.xの一番新しいのをダウンロードして展開、

$ mkdir /tool/frege
$ cd /tool/frege
$ unzip ~/Downloads/frege-repl-1.0.1.zip
Archive:  ~/Downloads/frege-repl-1.0.1.zip
  inflating: jline-2.10.jar
  inflating: frege-interpreter-1.0.0.jar
  inflating: frege-maven-plugin-1.0.5-frege-3.21.232-g7b05453.jar
  inflating: ecj-4.2.2.jar
  inflating: memory-javac-1.0.0.jar
  inflating: frege-repl-1.0.1.jar

REPLを起動します。

$ java -jar frege-repl-1.0.1.jar
Welcome to Frege 3.21.232-g7b05453 (Oracle Corporation Java HotSpot(TM) 64-Bit Server VM, 1.7.0**)

frege> 1+1
2
frege> quicksort [] = []

frege> quicksort (x:xs)   =  quicksort [y | y <- xs, y<x ] ++ [x] ++ quicksort [y | y <- xs, y>=x]

frege> quicksort [3,0,1,3,4,5]
[0, 1, 3, 3, 4, 5]

ghciでは関数とかを定義する際にlet文にすることが必要ですがfregeのreplではいりません。

コンパイルして実行する

前述のfrege-replのzip中にあったfrege-maven-plugin*.jarにはfrege本体のjarも含まれているので、これを使ってFregeソースコードJavaクラスファイルにコンパイルすることができます。とはいえ、frege-maven-plugin*.jarに入っているfregeは最新版ではない可能性があるので、最新版を使いたい場合こちらから直接ダウンロードする必要があります*2

まずソースを確認。

$ cat test.fr
module sample.Test where
quicksort [] = []
quicksort (x:xs)   =  quicksort [y | y <- xs, y<x ] ++ [x] ++ quicksort [y | y <- xs, y>=x]
main _ = print $ quicksort [1,3,2,4,5,0,7]

コンパイラを実行。

$ java  -cp /tool/frege/frege-maven-plugin-1.0.5-frege-3.21.232-g7b05453.jar frege.compiler.Main test.fr
runtime 4.304 wallclock seconds.

生成結果を確認。

$ ls -la sample
total 120
drwxr-xr-x  12 uehaj  wheel    408 12 21 05:59 ./
drwxr-xr-x  10 uehaj  wheel    340 12 21 05:59 ../
-rw-r--r--   1 uehaj  wheel   2445 12 21 05:59 Test$1Flc$1_3259.class
-rw-r--r--   1 uehaj  wheel   2448 12 21 05:59 Test$1Flc$4_3263.class
-rw-r--r--   1 uehaj  wheel    939 12 21 05:59 Test$IJ$1.class
-rw-r--r--   1 uehaj  wheel   1104 12 21 05:59 Test$IJ$2.class
-rw-r--r--   1 uehaj  wheel   1091 12 21 05:59 Test$IJ$3.class
-rw-r--r--   1 uehaj  wheel   1004 12 21 05:59 Test$IJ$4.class
-rw-r--r--   1 uehaj  wheel    674 12 21 05:59 Test$IJ$5.class
-rw-r--r--   1 uehaj  wheel   1593 12 21 05:59 Test$IJ.class
-rw-r--r--   1 uehaj  wheel   8570 12 21 05:59 Test.class
-rw-r--r--   1 uehaj  wheel  14999 12 21 05:59 Test.java

sample/Test.javaが生成されていますね。実行はこうです。

$ java -cp /tool/frege/frege-maven-plugin-1.0.5-frege-3.21.232-g7b05453.jar:. sample.Test
[0, 1, 2, 3, 4, 5, 7]
runtime 0.248 wallclock seconds.

これでわかるように、「module sample.Test where」というモジュール宣言をしたFregeソースは、FQCNが「sample.Test」であるJavaクラスにコンパイルされます。モジュール名のパッケージを除いた部分(Javaではクラス名)である「Test」は大文字で始まる必要があります。このモジュールで定義された関数は、sample.Testクラスのstaticメソッド定義にコンパイルされます。例えば上のquicksort関数は

final public static PreludeBase.TList quicksort(
  final PreludeBase.COrd ctx$1, final PreludeBase.TList arg$1
) {

こんなメソッドシグネチャメソッドコンパイルされるようですね。

Gradleでコンパイル

こちらにGradleプラグインがあります。とはいえこのプラグイン自身がどこかのリポジトリに上がってるわけではないので、このプラグインソースを自分のプロジェクトのbuildSrc配下に展開しておく必要があります。んで以下のようにして、開発するfregeソースはsrc/main/fregeに置きます。

apply plugin: "java"
apply plugin: org.gradle.frege.FregePlugin

repositories {
  flatDir name:"frege-lib", dirs:"lib"
}

dependencies {
  compile ':frege:3.21.232-g7b05453'
}

compileFrege {
  outputDir = project.file("$buildDir/frege")
  verbose = true
}

サンプルコード

ほぼHaskellなので例を挙げて紹介するのは省略します。自分がオフラインどう書くの問題をいくつかFregeで解いたものはこちら。ほとんどHakellで書いてからFregeに書きなおしてます。本体配布物に含まれている例はこちら
Real World Haskellの例をFregeで書き直そうというプロジェクトReal World Fregeというのもあります。

特徴を散発的に紹介

  • lambdaに複数引数は使えない。ただし、\x -> \y -> expは\x \y -> expと書ける(\x y -> expが駄目)。
  • 関数合成演算子の「.」はクラスのメンバー指定という意味に転用されている。関数合成には代りに •(BULLET,U+2022)を用いる。なお、互換性のために、「.」の前後に空白が置くもしくは「(.)」と書くと合成の意味になるようにしてあるそうです。
  • データ型は名前空間になるので、レコードフィールド名がトップレベルを汚染しない。runSTとかrunStateとかバリエーション作らなくてもrunで良いわけです。
  • 正規表現リテラルが使えて、パターンマッチに使える。
     frege> test ´^[ABC]´ = "start with A or B or C"
     frege> test "CDEF"
     start with A or B or C
     frege> test "GHI"
     frege.runtime.NoMatch: test at line 3 no match for value GHI
  • ランクN多相GHC拡張に相当するものを標準で使える。STの定義に使われている。
  • 型クラスの機能はHaskellにくらべ不完全とのこと。
  • モナドとかの型階層は違っている(MonadはApplicativeとか。結果としてApplicativeのpureはreturnにリネームされている)
  • 「パッケージから外部に公開するときにexport」するんじゃなくて「デフォルトは公開で、公開したくないときprivateを付ける」。abstractを付けるとすべてのConstructorがprivateになる。
  • 演算子は任意に定義できる(:で始まらなくても良い)。
  • 数値型はJavaのを使う(Int,Double,Float)
  • 文字列定数はjava.lang.Stringでありリストではない。charのリストと相互変換するにはunpack/packを使う。
  • Bool型はjavaのbooleanのAliasであり、代数データ型ではない。データ構築子True,Falseも無い。trueとfalseが予約語(リテラル)。
  • レイアウトルールは何か違うらしい。
  • read(s) は s.atoi
  • トレースするには以下をガードに入れる(便利)
   | traceLn (":"++show x) = undefined
  • QuickCheckの移植版もあるが使い方がよくわからない
  • javadocみたいなドキュメントコメントシステムがある
  • Eclipse Pluginもあるけど試してない

Javaとのインターフェース

時間不足により、今回は詳しい紹介を断念しますが、非常にエレガントです。Javaメソッドを何もせずに直接呼び出せる、というわけではなく、入出力の有無と状態変更をするかどうか(評価順を定めるかどうか)によって、IO aとST s aでラップした宣言をして呼び出します。副作用がなければ、pureと宣言します。たとえば以下はHashSetをFregeから使うための宣言で、native-genというnative宣言のジェネレータで生成しました。Javaメソッドがpureかどうかは今のところ人間が判定するしかないので、この自動生成ジェネレータはすべて安全サイドに倒して評価順の定まる処理の中で評価されるようにSTとして宣言されます。Imutableなクラスとそれに対するメソッドであれば、pureと宣言すれば純粋関数からも直接呼び値を得たり処理したりすることができます。
STモナドについてはこちら

data HashSet e = native java.util.HashSet where

  native new :: Int -> STMutable s (HashSet e)
              | Int -> Float -> STMutable s (HashSet e)
              | Mutable s (Collection e) -> STMutable s (HashSet e)
              | () -> STMutable s (HashSet e)

  native add :: Mutable s (HashSet e) -> e -> ST s Bool

  native clear :: Mutable s (HashSet e) -> ST s ()

  native clone :: Mutable s (HashSet e) -> ST s Object

  native contains :: Mutable s (HashSet e) -> Object -> ST s Bool

  native isEmpty :: Mutable s (HashSet e) -> ST s Bool

--  native iterator :: Mutable s (HashSet e) -> STMutable s (Iterator e)

  native remove :: Mutable s (HashSet e) -> Object -> ST s Bool

  native size :: Mutable s (HashSet e) -> ST s Int

instance Serializable (HashSet e)

まとめ

Fregeの最大の利点は、Haskellに似ている、ということです。そして、最大の障壁(?)の一つは、おそらく、Haskellに似ている、ということです。今のところ、Fregeを理解するにはHaskellの知識が色々な意味で間違いなく必須です。純粋関数型言語に興味があるというJavaプログラマが興味を持つきっかけとしては良いと思います。
今後の発展を期待します。

*1:命題論理と述語論理の公理化を最初に行なったドイツの先駆的論理学者ゴットロープ・フレーゲからの名前とのことです。

*2:REPLを本体側に同梱で配布して欲しいわ!

Bytecode DSL & Indy & Brainf*ck、副題「Indyを世界一簡単に扱う方法」です。

G*Workshop Z「JGGUG名物・ライトじゃないLT大会 - JGGUG G*ワークショップZ Sep 2013」でLTをしました。

http://www.slideshare.net/uehaj/gws-lt-20130920key

Bytecode DSL & Indy & Brainf*ck、副題「Indyを世界一簡単に扱う方法」です。

この資料は、以前の記事「indyでBrainfuckを実装してみよう!」の元ネタ情報であり、作っているのは同じBrainf*ckコンパイラですが、背景とか説明を加えたものになっています。

JVMの手軽なアセンブラが使えるようになったってことで、今度は他の言語のパーサやコードジェネレータを作ってみたいものですよね。本件のさらに詳しい情報は、そのうち公開予定の[JGGUG G*Magazine:title=http://grails.jp/g_mag_jp/] Vol7に解説記事を書きましたので、そちらを参照のこと。(追記: こちらこちらで読めます)

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日ぐらい。

Clojureの型ヒント

遅まきながら、一部でブーム?のClojureの本、「プログラミングClojure」読んでます。

プログラミングClojure

プログラミングClojure

ClojureJVM上のSchemeライクなlisp関数型言語で高速とのことです。

Clojureに興味があったのは、何で高速なのかを知りたかったということが一つです。マイクロなベンチマーク(1,2)を見るとなかなか速いらしい(少なくともGroovyよりは)。Lispも動的系の言語なのに、なぜその差が生じるのかなと。

まだ本を途中まで読んだ限りですが、高速であることの理由として、関数型だから副作用が無くてロックしないコレクションを持ってるとかもあると思うわけですが(まだそこまで読んでない)そういう高尚なところを除いて、一つわかったことを書いときます。

Clojureの場合、オプショナルタイピングじゃないけど、「型ヒント」なるものを関数の引数やローカル変数などに指定することができます。たとえば

(defn hogehoge [#^String str]
..
(.toUpperCase str ))

みたいに書けて、赤字の「#^String」がメタ情報としての型ヒント指定なのですが、これは引数strの型がjava.lang.Stringであることを示します。型ヒントをつけない場合は

(defn hogehoge [str] 
  ..
  (.toUpperCase str))

こうね。ちなみに引数を[..]で指定するのはClojure流で、字句的にはベクターリテラルだそうです。

話を戻して、型ヒントをつけないと上の場合toUpperCaseはリフレクションをつかって呼び出されるけど、型ヒントをつけると、

  ((String)str).toUpperCase()

みたいな型キャストを使ったコードが生成される。Javaと同等レベルの効率になるわけです。

メソッド呼び出しについて、リフレクションを使った呼び出しか、いわゆるvtableを使った呼び出しか、ということを切り替えられるということです。後者の方がもちろん速い。

さらに面白いのは、Clojureの場合、受け取った引数を別のメソッドの引数に渡したりする場合には、型チェックがスルーされるということです。
例えば、以下のコード

(defn hogehoge [^#String str]
   (println str))

のように、strのメソッドを呼び出さずに、他のメソッドにstrを受け渡すだけの場合、hogehogeに対してString型ではないオブジェクトを渡しても問題なく動くとのこと。おそらくバイトコードのコンパイル結果としてはJava

void hogehoge(String str) {
  System.out.println(str);
}

のように書いたのと同じように引数を「String」にコンパイルしているわけではなく、

void hogehoge(Object str) {
System.out.println(str);
}

のようにObject型で受け渡していると想像されます*1。その意味で、静的型では無くて、動的な強い型制約でもなくて、あくまで最適化・高速化のためのヒントでしかない*2

本当にそうかどうかは自信が無いのですが、Clojureが速いのは、ひとことでいうと動的型言語ではあっても動的言語ではないということなのかな?(Clojureメタ機能の有無や度合いについてはまだ読み終わってないので不明)。

Groovyのオプショナルタイピングとも似てるのですが、Groovyではメソッド呼び出し時に引数の型チェックが実行されます。なので

void func(String s){println s}
func(3)

というような呼び出しはエラーになります。また単なるキャストになるわけでもありません(なので実行速度向上にはあんまり貢献しない)。

あとは余談ですが、上の型ヒントは「メタ情報」なのですが、一般に、Clojureでは変数にメタ情報というものを紐付けられます。メタ情報には他に、Emacs Lispみたいなドキュメントコメントとか、「テストケース」があります。関数にその関数のテストケースを紐付けることもできるというわけです。アノテーションみたいなもんだと思いますが、応用の方向が面白いですね。

*1:その意味では、Javaと比しての動的なペナルティはありうる。キャスト実行命令が実行されるから。ただ、JITコンパイルでそのうち僅少化されると思うけど。

*2:まーこりゃコンパイル系Lispではあたりまえだのクラッカーな話ですけども