ろじかるんるんものがたり

病人が特に何も書かない。無駄だからだ。

Scala 2.12 における Java 8 への移行、対応について

お仕事は相変わらずないので結構ピンチです。

軽い気持ちでツイートしたら、ちょっとだけですが RT されたり like されたりしたのでちょっとまとめます。あんまり Scala 2.12 での Java 8 対応まとめみたいな記事、国内外問わず、公式にも非公式にも(知ら)ないなあと思ったので、簡単に…ロードマップにちらっとあるくらい?

SAM サポートの追加

Java 8 のラムダ記法を支える SAM(Single Abstract Method)…という用語は古くて、正確には Functional Interfaces といったほうがいいんですが、FunctionalInterface アノテーションとかあるしコミット名も SAM support なので SAM サポートの追加ということで。詳しくは JLS9.8 参照。

まず Java のおさらい。旧来の Java では関数オブジェクトのようなものを渡したい時には以下のように無名クラスを使って記述する必要がありました。

new Runnable {
  @Override
  void run() {
    // some processes...
  }
};

長い!Java 8 からはラムダ記法で以下のように書けるようになりました。

() -> {
  // some processes...
};

短い!ラムダ記法が利用できるのは abstract method を一つだけ持ってる interface に限る(default の話はややこしくなるので省略)のですが、まあ兎に角シンプルに書けるようになりました。

Scala 2.12 からは、Scala の関数オブジェクトも Functional Interfaces になるようにコンパイルされます。これは -Xexperimental オプションを使えば、2.11 でも利用可能です。気になる人は試して逆コンパイルしてみましょう。

それとは別に、Java 8 にはインターフェースが Functinal Interfaces であることを示すための FunctinonalInterfaces アノテーションが存在します。Scala 2.12 からは、Scala の関数オブジェクトに対応するクラスである FunctionN に付与するようになります。残念ながら SAM と違って 2.11 で -Xexperimental オプションを利用してもアノテートされません。これなんでだっけ…確か default 使った形に翻訳するからだったっけ…兎に角この変更により、Scala 2.12 で Scala コードをコンパイルすれば、Java 側から Scala高階関数(メソッド)をラムダ記法で呼び出せる(ようなクラスファイルが生成される)ようになります。やったぜ。必要になる機会あんまりない気がするけど…

逆に FunctionalInterfaces アノテーションが FunctionN に付与されていなくても、Scala 側から Java高階関数(メソッド)を呼び出すことはできます。有名どころだと Stream API とかですかね。Scala で Java8 の Stream API 使う機会もあんまりない気がするけど…

バイトコード生成フェーズの改善

Scala 2.11 までは、Scalaバイトコード生成フェーズは GenASM と呼ばれるものが存在しました。これに替わって GenBCode と呼ばれる新しい生成フェーズが実装されました。と書くと、いかにも Scala 2.12 のために追加みたいな感じに聞こえますが、実は大分前に実装されています。よほど古いバージョンでなければ Scala 2.11 でも利用可能なはずです。

おもむろに scalac -Y とか入力してみましょう。なんかたくさん出てくると思いますが。

-Ybackend:<choice of bytecode emitter>  Choice of bytecode emitter. (GenASM,GenBCode) default:GenASM

というのが出てれば利用可能です。

これに伴いというわけでもないんですが、JVMバイトコードを生成する前に、一旦よりポータブルな形式の中間表現を生成する ICode と呼ばれるフェーズがあったのですが、2.12 からはなくなります。以前は .Net 向けのバイトコードも出力できたんですが、これで公式のコンパイラとしてのサポートはなくなります。というか .Net サポートは既にないんですが、これで跡形もなくなるという感じですね。必要ならコンパイラプラグインでやりましょう。今のところそういうプラグインは聞いたことがないですが。

閑話休題。GenBCode は兎に角日々色々と改善されているわけですが、とりわけ Scala 2.12 と Java 8 という文脈で重要なのが trait のコンパイルです。現状の trait のコンパイル結果については、まだ自分が Scala 触り始めたての頃にはてなダイアリーで少し書きました。うわーもう五年前だ死にたい。ようは分割コンパイル(separate compilatin)できなくて困るわけですが、これが Java 8 で導入された default メソッドにより改善されるようです。進捗的には確かまだバグが残ってて wip だったと思います。

default メソッドというのは、interface に実装を伴ったメソッドを持たせるための Java 8 の新機能で、大丈夫かよみたいな機能なんですが…細かい説明は面倒なので省略しますが、trait を default メソッドを持った interface として単純に翻訳可能になります。本当に説明が面倒臭いので Scala Days 2015 のプレゼンのスライド読んでください。pdf です。これによって分割コンパイルできるようになったりします。やったぜ。

他にも大きい改善点として、関数オブジェクトの翻訳の改善があります。現状の Scala の関数オブジェクト(FunctionN のオブジェクト)は、適当に mangling されたクラスのオブジェクトとして生成されます。つまり関数オブジェクト書けば書くだけどんどんクラスが増えていって、Scala による Android 開発で問題になったり、jar が膨れ上がったりします。これが、Java 7 で導入された invokedynamic を利用することで static メソッドとして翻訳されるようになります。これならクラスファイルがどんどんできたりはしません。やったぜ。でも Java 8 に移行したら Android 開発には使えない気がしますね…より詳細な説明は前述のスライド参照。

ところで関数オブジェクトの翻訳の進捗は知らないんですが今どうなってるんでしょう。delambdafy 自体はかなり前に追加されてましたが、ボクが最後に実装読んだ時にはまだ invokedynamic を使うようには翻訳してなかった気がする。詳しい人教えてください。自分でコード読めといわれたらまあそれまでですが…おじさんは病人なんだ…病人を労わってくれ…

その他、コンパイラによる optimization の追加等があります。前から一応定数畳み込みとか色々あったんですが、大して効果なかったようで、オプション指定しないと使われません。かわいそうな dragos さん…(EPFL で scalac に optimizer 追加して論文書いたりしてた人。今の所属は忘れました)。追加される optimizer は従来のそれとは違うらしいです。最後に自分が scalac のコード読んでた頃にはまだこの辺そんなに進んでなかったか、本家のリポジトリ外で進んでたとかで見た記憶がないので、詳細は良く知りません。前述のスライド…にも大したことは書いてないな。ウーム。

まとめ。

  • Functional Interfaces に対応するよ!Java 8 との Interoperability が向上!
  • trait の翻訳方法改善するよ!separate compilation とか可能になって再コンパイル地獄減るよ!
  • 関数オブジェクトの翻訳方法改善するよ!クラスファイル大量にできて jar 太ってたけど痩せるよ!

という感じです。他にもライブラリレベルとかで細々とやってるようですが、大きいのはこの三つ。

そろそろ口座残高が気になるので仕事がしたい、ガルパンテレビシリーズの BD 全巻買いたい(劇場版は購入済みです)。以上、近況と Scala 2.12 の Java 8 対応の簡単なまとめでした。簡単とかいったけれど、想定の三倍くらい長くなった。

…情けないことに、以前より処理系の実装とか見てないというか全く見ないし、Scala についての記事なども追っかけなくなったので、間違ってる部分や補足すべき部分があったら、記事にコメントするなり、最初の自分のツイートにリプライするなりお願いします。おしまい。

追記:早速指摘いただいたというか貼ってる PR に 2.11.0 ってバージョン書いてあるのに何を間違えたんだろう…