Re: いまさらだけど、Java言語にはクロージャーがない
id:ryoasaiさんと先日会社で話した内容が、「いまさらだけど、Java言語にはクロージャーがない」にまとめられたけれど、関数リテラルが利用できるという文法の話と、引数以外の外部変数(自由変数)が参照できるという機能の話が混ざっている気がします。
前者の意味では現在の Java 6 にはクロージャがないけれど、後者の意味では Java でも内部クラスで同様なことが実現できます。他の言語が内部的にクロージャをどう実現しているか詳しくは知りませんが、クロージャを導入する土台は既にあると考えています。C/C++ でクロージャを実現するよりはかなり敷居が低いかと。
「想像以上にガラパゴス化した日本のIT業界?」で、Groovy と Scala の比較もされているようなので、クロージャに関して、Java、Scala、Groovy の違いを整理しておこうと思います。
記事で引用されている クロージャ - Wikipedia にある JavaScript の例を使用します。
function newCounter() { var i = 0; // ↓これがクロージャ。 return function() { // 関数の外部の変数を参照/変更する。 i = i + 1; return i; } } c1 = newCounter(); alert(c1()); alert(c1()); alert(c1()); alert(c1()); alert(c1());
Wikipedia の説明では、他の言語の例も書かれているのですが、同じ題材で書かれておらず比較しにくいので、この記事では他の言語でも同じ例を使用することにします。
Java の場合
先程書いたように、Java では内部クラスを使用すると同様のことができます。ただし、Java では内部クラスから参照する変数は final でなければならないので、値を変更するためには、オブジェクトや配列でラップする必要があります。
class NewCounter { public static void main(String[] args) { Function0<Integer> c1 = newCounter(); println(c1.apply()); println(c1.apply()); println(c1.apply()); println(c1.apply()); println(c1.apply()); } static Function0<Integer> newCounter() { // 変更可能とするためオブジェクトでラップする。 final IntHolder i = new IntHolder(0); // ↓これがクロージャ。 return new Function0<Integer>() { public Integer apply() { // 関数の外部の変数を参照/変更する。 return ++i.value; } }; } static void println(Object o) { System.out.println(o); } }
ここで、Function0、IntHolder の定義はこんな感じ。
interface Function0<R> { R apply(); } class IntHolder { public int value; public IntHolder(int value) { this.value = value; } }
JavaScript の例と比較すると繁雑ですが、やりたいことは実現できていると思います。
Scala の場合
同じ例を Scala で記述すると以下のようになります。
def newCounter() = { var i = 0 // ↓これがクロージャ。 () => { i += 1; i } } val c1 = newCounter() println(c1()) println(c1()) println(c1()) println(c1()) println(c1())
Java と比較してずいぶん簡潔に記述できます。
クロージャをどのように実現しているか見るために、コンパイルして class ファイルを生成後、javap してみます。このままだとコンパイルできないので、以下のコードで全体を囲みます。
object NewCounter extends Application { // ... }
コンパイルすると class ファイルが3つ生成されます。
まず1つ目は NewCounter。メインメソッド main が定義されています。
public final class NewCounter extends java.lang.Object { public static final void main(java.lang.String[]); public static final scala.Function0 c1(); public static final scala.Function0 newCounter(); // ... }
続いて NewCounter$。Scala には static メソッドが存在しないので、メソッドの本体を格納するためのクラスです。
public final class NewCounter$ extends java.lang.Object implements scala.Application, scala.ScalaObject { public void main(java.lang.String[]); public scala.Function0 newCounter(); public scala.Function0 c1(); // ... }
最後にクロージャ(関数)の実体となる無名クラスです。
public final class NewCounter$$anonfun$newCounter$1 extends scala.runtime.AbstractFunction0$mcI$sp implements java.io.Serializable { public static final long serialVersionUID; public NewCounter$$anonfun$newCounter$1(scala.runtime.IntRef); public final int apply(); public int apply$mcI$sp(); public final java.lang.Object apply(); // ... }
内部的には Java で実装した場合とだいたい同じよう仕組みで実現していることが分かります。
このあと取り上げる Groovy と比較すると、Scala は型推論によりメソッドの引数や戻り値の型がきちんと定義されているのが特徴的です。また、変数の可変性を val と var で区別できるのもメリットです。
反面、(id:ryoasaiさんも言及されてますが)Java と文法の差異が大きいことは敷居を上げてしまっているように思います。あと、スクリプトのままコンパイルできなかったり、i += i
の式の評価が Unit になっているなど、細かい点で使いにくい箇所が見受けられます。
Groovy の場合
続いて Groovy で同じ例を実装してみます。
def newCounter() { def i = 0 // ↓これがクロージャ。 return { ++i } } def c1 = newCounter() println(c1()) println(c1()) println(c1()) println(c1()) println(c1())
JavaScript や Scala と記述量はほとんど変わりません。
Scala と同様に、コンパイルして class ファイルを生成後、javap してみます。Groovy はスクリプトのままコンパイルできました。class ファイルは2つ生成されます。
まず1つ目は NewCounter。メインメソッド main や関数 newCounter が定義されています。
public class NewCounter extends groovy.lang.Script { public static void main(java.lang.String[]); public java.lang.Object run(); public java.lang.Object newCounter(); // ... }
そしてクロージャ(関数)の実体となる無名クラスです。
class NewCounter$_newCounter_closure1 extends groovy.lang.Closure implements org.codehaus.groovy.runtime.GeneratedClosure { public NewCounter$_newCounter_closure1( java.lang.Object, java.lang.Object, groovy.lang.Reference); public java.lang.Object doCall(java.lang.Object); public java.lang.Object doCall(); // ... }
Scala と比較するとメソッドの引数や戻り値が Object 型になっており、Java との相互運用やパフォーマンス面では不利かなと思いました。
個人的には、Groovy に近い文法(ただし、変数の val、var の区別はあった方が良いと思う)で、Scala の型推論や関数型の機能が使えるとベストだったなと思うのですが、どうでしょうね。