2011年8月28日日曜日

和訳: Project Lambda (前半)

注:この文書はProject Lambdaの提案を和訳したものです。訳の正確性は保証しません。
この文章におかしなところがあればオリジナルを参照してください。

Brian Goetz 2010/10/10

この文書はJava言語にラムダ式を追加する案です。このスケッチはMark Reinholdによって 2009/12に作られた原案をもとに作られました。前のバージョンは2010年7月に発表されました。

1.背景: SAMタイプ


Java言語には一種のクロージャとして匿名内部クラスがあります。このクロージャが不完全な理由として幾つか考えられますが,主に以下のような理由です。


  1. 構文がかさばっている
  2. finalでないローカル変数を参照できない。
  3. return, break, continue, this の意味が透過的ではない。
  4. 外側の制御フローを操作できない。

Project Lambdaはこれらすべての問題を解決することが目的ではありません。しかし現在の草案ではこれらの幾つかを解消します。問題(1)は大幅に解消されます。問題(2)はコンパイラがfinalであるかを推測することで(つまりコンパイラはfinalと書かれていなくても,実際には変更されていなければ参照できるということです)改善されます。(3)は,ラムダ式の中でのthisはレキシカルスコープ上のものを指すことでを改善されます。今回はこれ以上この問題に取り組むつもりはありません。(例えば制御フローに対して取り組むつもりはありませんし,変更可能なローカル変数を参照可能にするつもりもありません。)

Javaで一般的なコールバックを定義する場合,次のようにコールバックメソッドを持つインタフェースを利用します。

public interface CallbackHandler {

public void callback (Context c);
}

このCallbackHandlerインタフェースはただひとつの抽象メソッドを持つという便利な性質を持っています。このような性質を持つインタフェースや匿名クラスは多く,例えばRunnableやCallable,EventHandler,Comparatorなどがそうです。この性質を持つ型を以降,SAM型(Single Abstract Method)としましょう。この性質,SAM型かどうかというのは,型システムで表現されるのではなくコンパイラが構造を読み取って判別されます。

匿名内部クラスの最も辛いところはコードが肥大化してしまうことです。CallbackHandlerを利用して何かする関数は,多くの場合次のように匿名内部クラスを利用して記述します。

foo.doSomething(new CallbackHandler() { 

public void callback(Context c) {
System.out.println("pippo");
}
});

このように匿名内部クラスを用いるとソースコードが縦に伸びるという問題があります。この例では1行のステートメントを囲うために5行も記述しています。

2.ラムダ式


ラムダ式は匿名関数です。匿名内部クラスという機構をラムダ式というシンプルな機構で置き換えることで行数が増える問題に対処します。ラムダ式を言語に導入するひとつの方法は言語に関数型を追加することです。これには幾つかの欠点があります。


  • structualな型システムとnominalな型システムが混ざってしまう点。
  • ライブラリのスタイルが別れてしまう点。(幾つかのライブラリでは今までどおりコールバックオブジェクトを使い,それ以外では関数型を使うだろう)
  • 総称(ジェネリック)型がイレイジャであり開発者から隠蔽されているイレイジャという仕組みを開発者にさらしてしまう点。例えば今のJava総称型ではメソッド m(T -> U) と m(X -> Y) を区別できないため,ふたつの関数をオーバーロードできません。

そこで私たちは代わりに「知られているやり方」を用いるという道を選びました。すなわち、既にライブラリにSAM型が広く用いられていることから,SAM型にラムダ式の考え方を取り入れることで,コールバックオブジェクトの作成を簡単にすることにしました。

ラムダ式の例を示します。

#{ -> 42}

# { int x -> x + 1 }

ひとつ目は引数を取らず42を返す関数です。ふたつ目はひとつの整数引数xを取りx+1を返す関数です。

ラムダ式は #{},引数のリスト,トークン「->」,ラムダの本体のよっつで構成されます。引数を取らないラムダ式,例えば一つ目の式のようなの場合,トークン「->」は省略して次のように記述できます。

#{ 42 }

ラムダ式の本体は,ひとつの式あるいは(メソッド本体のような)ステートメントのリストのいずれかになります。ひとつの式で表す場合,returnとセミコロンは必要ありません。この簡略記法は,多くの場合ラムダ式が上の例のように短いだろうという想定に基づいています。簡潔な式ではreturnは行を横に伸ばすノイズでしかないといえます。

3.SAMの変換


SAM型は戻り値の型,引数の型,検査例外の型で表すことができます。同様にラムダ式も戻り値の型,引数の型,例外の型で表すことができます。
次の条件を満たす匿名内部クラスAが定義できる場合,ラムダ式eはSAM型Sに変換可能です。


  • AがSの子クラスであり,Sの抽象メソッドと同じ名前を持つメソッドMが宣言されている。
  • Mのシグネチャ(この場合は戻り値の型,引数の型,検査例外の型)とラムダ式eのシグネチャが一致している。

ラムダ式の戻り値の型と例外の型はコンパイラによって推定されます。一方引数の型は明示的に書いてもよいですし,代入されるコンテキストにより推定されることもあります。
ラムダ式をSAM型に変換した場合,SAM型のインスタンスの抽象メソッドを実行すると,ラムダ式の本体が実行されることになります。

SAMへの変換はこのようなコンテキストで発生します。

CallbackHandler cb = #{ Context c -> System.out.println("pippo") };

この例ではラムダ式の引数の型はContext,戻り値の型はvoid,そして検査例外はありません。このためSAM型であるCallbackHandlerに変換できます。

4.変換対象の型


ラムダ式はSAM型の変数に変換される文脈,つまり代入,キャスト,メソッドの実行という文脈でのみ記述可能です。以下はラムダ式を使った有効な例です。

Runnable r = #{ System.out.println("Blah") };

Runnable r = (Runnable) #{ System.out.println("Blah") };
executor.submit( #{ System.out.println("Blah") } );

SAMに変換できる文脈ではないため,以下のようなラムダ式の利用は無効です。

Object o = #{ 42 };

メソッドの実行という文脈では,実行されるメソッドのシグネチャに代入可能な集合に対して試してみることで,メソッドの引数となっているラムダ式の変換対象の型を推論できます。これによりオーバーロードの解決においてさらに幾つかの手間が加わります。通常の解決方法では,すべての引数の型を計算し,次に適用可能なメソッドの集合の中から最も適切なメソッドが選び出されます。実引数であるラムダ式の型の推論は,他の引数の型を計算した後に行われますが,メソッドを選び出すよりは前に行われます。メソッドの選択は推論されたラムダ式の型を使って行われます。

ラムダ式の仮引数の型はラムダ式が変換される対象の型から推論できます。そのためCallbackHandlerを次のように書くことができます。

CallbackHandler cb = #{ c -> System.out.println("pippo") };

この例では仮引数cの型はラムダ式が変換される型CallackHandlerから推論することができます。

仮引数の型をこのように推論することができれば,望ましい設計上のゴール「縦長問題を横長問題に変換するな」に近づけてくれます。私たちは,ラムダ式の本質と同じくらい短いコードを読み手が読み進めていけるようになるのを望みます。

ユーザは明示的にキャストを用いて変換対象の型を選ぶこともできます。これによってコードが明らかになることもありますし,複数のオーバーロードがあってコンパイラが正しい対象の型を選べないときに解消できるかもしれません。例を挙げます。

executor.submit(((Callable) #{ "foo" }));

もし対象の型が抽象クラスであれば,コンストラクタの実引数を書く場所がありません。そのため引数のないコンストラクタを実行する必要があります。しかしコンストラクタに引数を渡したい場合はいつも内部クラスを使うという選択肢があります。

5.ラムダ本体


ラムダ式の本体には単純な式のほかに,メソッドの本体同様ステートメントのリストを持つことができますが幾つかの違いもあります。break文やcontinue文はトップレベルでは許可されていません。(もちろんラムダ本体の中でbreak文やcontinue文を使うことは可能です。しかしその場合breakやcontinueの指すループもラムダ本体の中になくてはなりません。)return文は複数のステートメントからなるラムダ式の中で利用可能です。これはローカルなreturn,つまりラムダ式の戻り値を指します。複数のステートメントからなるラムダ式の型は,戻り値の型の集合を合わせたものと推論されます。メソッド同様,複数のステートメントからなるラムダ式はどの制御パスであっても必ず値(あるいはvoid)を返すか例外を投げる必要があります。

6.ローカル変数の参照


内部クラスから外のスコープのローカル変数を参照する現在のルールはとても制限されています。finalで修飾された変数のみが参照可能です。ラムダ式では(一貫性のため,内部クラスのインスタンスも同様にするでしょうが)この制限を緩和し,修飾子にかかわらず実質的にfinalでさえあれば参照可能にします。(簡単に言えば,final修飾子をつけてもコンパイルエラーにならないローカル変数をfinalとみなすということです。これは型推論の一種と考えても良いでしょう。)

これは変更可能な変数を参照することを認めているわけではありません。以下のような定型文を考えてみましょう。



int sum = 0;
list.forEach(#{ e -> sum += e.size(); });

この処理は本質的に逐次的に実行される必要があります。すなわち競合状態を起こさずにこのようなラムダ式の本体を書くのはとても難しいことです。変更可能な変数を参照しているスレッドからラムダ式の処理を逃さないよう(なるべくコンパイル時に)強制しないかぎり,発生しうる問題は解決しうる問題よりずっと多いでしょう。

0 件のコメント:

コメントを投稿