OpenJDK の今後に関する事態も落ち着きを見せ,PlanB によってラムダの JDK 8 (あるいはそれ以降) への先延ばしが確定した今,ラムダ自体は今後どのようになるのだろうか?
ラムダに関する最初の提案 は,Java 言語への関数型(functional type) の導入なども含んだ,かなり広範囲なものだった。しかし Java の型システムとの接点に根本的な問題が存在したため,新たな関数型 (あるいは構造的型付け/structural typing) の提案は撤回され,既存のクラスベース (あるいは名義的型付け/nominal typing) の方法が支持される結果に落ち着いた。ある意味これは,X
が Object
のサブタイプならば array[X]
は array[Object]
のサブタイプである,という事実が原因である。例外をスローする関数と組み合わせた場合,Java の型システムを損なう可能性があるのだ。
それ以降のラムダに関する提案では,クラスの匿名インスタンスの提供が話題の中心とされてきた。実質的にこれは,匿名内部クラスで使用される定型的なコードの置き換えとなるものだ。最も 新しい提案 によると,ラムダ はそれぞれ SAM 型と呼ばれる型のインスタンスになる。SAM 型 (SAM Type - Single Abstract Method Type) とは,抽象メソッドをひとつだけ所有するインターフェースあるいは抽象クラスである。既存の内部クラスの多くは暗黙的に SAM 的アプローチに従っている (Runnable
や Comparable
など) ことから,これはラムダの適用が予想されるユースケースに幅広く適応する方法だ。
最新のドラフトでは,ラムダからの復帰を指示する独立したキーワード yield
の使用という,提案に対して異論の多かった構文要素のひとつに対処している。(この目的に return を採用しなかった当初の理論的根拠は,ラムダを含むメソッドからの復帰を可能にすることにあった。しかし一方では,既存のコードをラップしてインライン修正に置き換える "透過的ラムダ" を望む声がある。いずれにしても Java では,Smalltalk のようなブロック表現やコントロール構造が実現できないので,完全な透過性が実現できるとは考えにくい。)
もうひとつの変更は,ラムダ引数の構文に関するものだ。以前のドラフトでは '平方' を求めるラムダを #(int a) { a*a }
と記述していた。現行の案ではこれを #{a->a*a}
という,Java のメソッド構文とは少し違うが,よりコンパクトな形式に置き換えている。コンパイラがこの式から型を推論できる場合 (たとえば SAM 型へのアサインや,既知の型の引数を持ったメソッドの明示的な呼び出しなど) については不要だが,そうでなければ (上の例であれば,a
は int
,float
その他の場合がある) #{int a -> a*a}
のような型指定が必要である。
他の関数型言語に比較すれば初歩的ではあるものの,型推論の追加は,ラムダ式の記述に必要な文字数を相当に削減する。動的型付け言語の支持者であれば,静的コンパイル言語におけるこれらの機能を,間違いなく有益なものと考えるだろう。ただし,コンパイル言語であっても (Scala のように) さらに強力な推論機構を持つものがあることに留意すべきだ。Javaの型推論がシステムの他の部分に拡張されるかどうかは未定だが,Project Coin では代入での一般型推論をすでにサポートしている。
さらに異論があるのは,ラムダにおける this
の意味が以前のドラフトとは逆転してることだ。これまでは内部クラスにおける解決法と同じように, this
がラムダのインスタンスを,Outer.this
がそれを包含するクラスを参照するものだった。現在では this
は包含クラスを示して,ラムダそれ自身を参照する正式な方法は失われている。ドラフトでは代入時点での自己参照を行う可変定義を導入して,これに対処しようとしている。言い換えれば,現在の Java コードでは無効な int a = a + 1;
というような記述を可能にしようというのだ。ドラフトでの例は次のようなものだ。
Timer timer = ... final TimerTask t = #{ if (somethingHappened()) // cancel the timer task that this lambda represents t.cancel(); else System.out.println("foo"); }); timer.schedule(t);
ここでの問題は #{..}
の型が匿名の MethodHandle
という,実質的に型の概念を持たないものである点だ。その代わり,これがローカル変数の型に割り当てられている場合は SAM の事実上のサブタイプに昇格される。そのためメソッドのハンドルである #{..}
に対して,それを包含する (SAM の) ラッパが作成される。これによって自分自身を再帰的に起動したり,SAM 型の他メソッドを呼び出すことが可能になる。ここでのテーマは,メソッドハンドルが自身のタイプを暗黙的に知ることができない (それはメソッドが参照される場所で,コンパイラが引数の型を推測することによって初めて分かる) ため,サブクラスのインスタンスからの再帰呼び出しの実装や,フィールドあるいはメソッドへのアクセスには使用できないことだ。
現時点でドラフトは十分に成熟していて,実装の進捗も良好である。JDK 7 には間に合わなかったものの,JDK 8 で採用された場合に重大な影響を起こすような問題はない。拡張メソッド (別名 defender method) と例外処理に関する既存動作に変更はない。実際にただひとつ存在する未解決の問題は,MethodHandle
は Serializable
か,そうでないなら code>Serializable なオブジェクトに設定された場合の振る舞いはどうか,ということだ。
この最新の進捗状況について,読者はどのような感想をお持ちだろうか?