きしだのはてな このページをアンテナに追加 RSSフィード

2013-05-22(水) Java8のlambda構文がどのようにクロージャーではないか

[][]Java8のlambda構文がどのようにクロージャーではないか 15:04 Java8のlambda構文がどのようにクロージャーではないかを含むブックマーク

Java8にlambda構文が入りましたが、これはクロージャーではない、とされています。

では、どのように「クロージャーではない」のか、ちょっと見てみます。


まず、lambdaを返すメソッドを定義します。

public static Supplier<String> createMessenger(String name, String address){
    return () -> {
        return String.format("私は%s、%sに住んでる", name, address);
    };
}

呼び出すと、こんな感じでSupplierを受け取ります。

Supplier<String> messenger = createMessenger("きしだ", "ふくおか");

このSupplierを実行すると、次のようになります。

System.out.println(messenger.get());

私はきしだ、ふくおかに住んでる


このSupplierが、name,addressの情報を保持しているように見えます。


lambda中では、次のようにして、lambdaの外側の変数を使っています。

return String.format("私は%s、%sに住んでる", name, address);

lambdaがクロージャーであるかどうかという話では、どのようにしてlambdaの外側の変数を使っているかが大事になります。

クロージャーというのは、言語の実装でいえば、関数と変数環境をあわせたものです。変数環境というのは、変数を保持するテーブルだと考えるといいと思います。変数が使われるときに、変数の内容を取ってくるために使われるテーブルです。

具体的には、ここでのClosureケースクラスの定義がわかりやすいです。

Scalaでパーサーを作ってみる〜10:レキシカルスコープとクロージャ - きしだのはてな


ここで、次のようにEnvironmentとFuncをまとめたものとしてClosureを定義しています。

case class Closure(env: Environment, func: Func)

逆に言えば、関数と変数環境をあわせたものでなければ、クロージャーではないということになります。

つまり、Javaのlambdaは、関数と変数環境をあわせたものではないので、クロージャーではないということです。


ためしに、上記のlambdaを使ったコードのクラスファイルをjavapで逆コンパイルしてみると、次のようなメソッドが追加で定義されています。

private static java.lang.String lambda$0(java.lang.String, java.lang.String);

lambda構文で引数をもたない関数を記述したのに、実際に生成されたメソッドは引数をふたつもつものになっています。そして、この引数に、lambdaの外側で定義した変数が渡されてきます。

Java8のlambdaでは、変数環境が渡されるわけではない、ということですね。


では、クロージャーじゃなければ何が困るかという話になります。

これは、もしlambda構文がクロージャーなら何ができるか考えてみるとわかります。クロージャーであれば変数環境が渡されてくるので、クロージャー内部から外側で定義した変数の内容を書き換えることができるはずです。

試しに先ほどのコードでlambda内部で変数nameの内容を書き換えてみます。

return () -> {
    name = name.trim();
    return String.format("私は%s、%sに住んでる", name, address);
};

そうすると、次のように「ラムダ式から参照されるローカル変数は、finalまたは事実上のfinalである必要があります」というエラーになります。

f:id:nowokay:20130522144524p:image


lambdaで外側の変数を使うときには、final定義されているか、「事実上のfinal」つまり初期化以外では値が代入されていない変数である必要があります。

ということは、lambda内でlambda外の変数を使おうとすると、lambda内部だけではなくlambdaの外でも変数の値を書き換えてはいけないということです。


ただ、lambda内で使う外部の変数を書き換えてしまうと、感覚的ではない動きになることがあり、プログラムの不具合につながりがちです。そのため、実際問題としてはむしろlambda内で使う外部の変数は書きかえれないほうがよいとも言えます。


JavaScriptでよく見かける例に、次のようなものがあります。

for(i = 1; i <= 10; ++i){
  button[i].onclick = function(){ alert(i + "番目のボタンがおされた"); };
}

n番目のボタンが押されたとき「n番目のボタンがおされた」というメッセージを出したいのに、実際にはどのボタンを押しても「11番目のボタンがおされた」となってしまうものです。


Java8で書き換え可能な変数が使えたとすると次のような感じです。

List<Supplier<String>> list = new ArrayList<>();
for(int i = 1; i <= 10; ++i){
    list.add( () -> String.format("%d番目", i));
}
list.forEach(s -> System.out.println(s.get()));

ぱっと見、「1番目」から「10番目」までが表示されそうです。


finalの制限は配列を使うことで回避可能なので、実際に実行可能なコードだと次のような感じになります。

List<Supplier<String>> list = new ArrayList<>();
for(int[] i = {1}; i[0] <= 10; ++i[0]){
    list.add( () -> String.format("%d番目", i[0]));
}
list.forEach(s -> System.out.println(s.get()));

実行してみると、「11番目」と10回表示されます。こういうコードが自然に書けてしまわないのは、いいことではないかと思います。


結論としては、「Java8のlambdaはクロージャーではないけど、クロージャーでやりたいことはできるし、やってはいけないことができないようになっているので、特に問題はない」と言えると思います。