「クロージャの定義」

Java7のクロージャの提案者の一人,Neal Gafterのブログが大変参考になるので,ちょっと野良翻訳してみよう.

クロージャの定義

Java 言語にクロージャを追加しようという我々の提案に関して混乱があるようだ.そもそも,Javaにはすでに無名インナークラスという形で,クロージャがあるのではないか? すでにあるものをなぜまた追加しようというのか? 一部の人々には,我々の提案には,クロージャとは関係ないものが含まれているように思われているようだ.例えば,control invocation 構文,null型,Unreachable, 型パラメータ付きthrows,関数インターフェイス型,「非ローカル」な returnなどがそうだ.Javapolisでの講演で,なぜこれらの機能が提案に含まれているのかを,これまで不可能だったことを可能にするための実用的な観点から説明したつもりだ.しかし,この講演は別の疑問を招いてしまった:ではなぜそれをJavaの「クロージャ」と呼ぶのか? このブログポストで,クロージャの定義が,われわれの提案の機能とどのように関係しているのか,そしてどの機能が定義から導かれているのか(もしくは導かれていないのか)を示そう.

クロージャの定義を議論する前に,この言葉が導入された歴史的な文脈を理解しておこう.

Lisp言語が,1950年代に John MacCarthy らによって,M.I.T.でつくられた.この言語の機能の一つに,ラムダと呼ばれる関数の値を持つ式があった.「ラムダ」という名前は,数学的形式論のλ-calculusからとられたものだ.Lispはこの形式論に基づく言語ではないのだが,ラムダはλ-calculusにおける役割と大体同じ役割,つまり関数の値を持つ式の構文という役割を,Lispでも果たしている.McCarthyは,Lispが非常に効率的に実装できるように,理想的にはコンパイルできるように,設計されるべきだと考えていた.この効率への欲求が,言語の設計に影響を与えた.

Lispは動的スコープと呼ばれるスコープルールを用いていた.論理的には,動的スコープの言語では,変数参照が評価されると,実行時環境はその変数名が定義されているスコープを見つけるまでコールスタックを検索しなければならない.しかし実際には,それぞれの変数名に対して現在の値をキャッシュする値セルをかんりすることによって,定数時間で,変数参照を解決することが可能だった.動的スコープは,インタプリタでもコンパイラでも簡単に実装できた.一部のとても賢い人たちは,動的スコープをただ利用するだけでなく,動的スコープに深く依存した,今でいうプログラミングパターンを編み出した.しかし,すぐに動的スコープにはFUNARG問題と呼ばれる,厄介な問題があることがわかった.

さて,時間を1970年代に進めよう.ラジオからは,Elton John, Emerson Lake & Palmer, Joni Mitchell, The Captain and Tennille, John Denver, Paul Simon, Paul McCartney and Wings, ABBA, David Bowie, Janis Ian, Aerosmith, Fleetwood Mac, Heart, Queenが流れていたころだ.このころは,いくつもの有名なLisp方言が使われていた.InterLisp, MacLisp, UCI-Lisp, Stanford Lisp 1.6, ユタ大学 Standard Lispなどで,これらはすべて動的スコープだった.そのようななかで,Guy Steele と Gerald Jay Sussman が,非常に単純なLisp方言であるSchemeを開発したのだ.

Schemeには,他のLisp方言とは異なる点があった.Schemeはλ-calculusや他の数学的記法と同様にレキシカルスコープだったのだ.つまり,変数参照は,外側のラムダ式が評価された時点でアクティブなラムダ式をレキシカルに取り囲む変数定義に束縛される.

このセマンティクスを実装の観点から説明するためにクロージャが導入された.ラムダ式を評価するとクロージャが作られる.クロージャは,オブジェクトとして表現された関数で,ラムダ式の中で使用されていて,ラムダ式の外で定義されているすべての変数への参照を含む.このような変数を自由変数と呼ぶ.後で,クロージャオブジェクト,つまり関数に引数を適用すると,クロージャに取り込まれた変数束縛が,コード中の自由変数に意味を与える.クロージャは単なる抽象的な言語要素ではなく,その実装法でもあるのだ.

そのころのLispコミュニティの多くの人たちにとって,クロージャを用いたLisp方言は意味をなさなかった.このような言語は,一般的なプログラミングテクニックを使えないようにするだけでなく,明らかに効率が悪いのだ.しばらくの間,この件に対する議論がなされ,Guy Steeleは一連の「Lambda the Ultimate _____」 (_____ には Imperative, Declarative, GOTO, Opcodeが入る) と題する論文を発表し,レキシカルスコープのラムダ式(クロージャ)の強力さを説明した.数年早送りすると,議論にはだいたい決着がついた.レキシカルスコープが正しく,動的スコープは間違っていたのだ.われわれは教訓を得た.クロージャという言葉は,レキシカルスコープの無名関数のことを意味するにもかかわらず,バグや実装の効率などのたくさんの理由で,その意味を取り違えるということがあり得るということ.また,言語の設計が実装を駆動するべきであって,その逆ではないこと,である.ラムダや無名関数値があろうと無かろうと,事実上すべての言語で,動的スコープではなく,レキシカルスコープが用いられている.しかし,クロージャの基本的な定義は,その起源がLispにあることを物語っている.

クロージャは,レキシカルな文脈で自由変数束縛を捕捉する関数である

このころ,Smalltalkがあらわれた.Smalltalkは,最も純粋で単純なオブジェクト指向言語で,すべてがオブジェクトである.オブジェクト指向言語では,レキシカルスコープに少し変更が必要になる.すべての名前をレキシカルスコープで束縛するのではなく,メソッド内に現れる自由変数は,そのメソッドが属するオブジェクトのスコープで束縛するのである.言い換えれば,メソッドに登場する名前は,「現在の」オブジェクトのメンバに束縛される.現在のオブジェクトは,selfという名前でアクセスできる.もう一つの些細だが面白い点はSmalltalkでは "^ 式" という構文で,メソッドから脱出できることである.この事実が持つ重要性は後で見る.

Smalltalkでは,メソッドだけがコード抽象方法ではない.ブロック式を書くことができる.ブロック式は本質的にラムダである.初期のいくつかの方言ではブロックに制約があったが,ほとんどの近代的なSmalltalkにはそのような制約は無く,Schemeのラムダと完全に等価なものになっている.Smalltalkのブロック内の自由変数は,ブロックの外側のスコープに束縛される.多くの場合外側のスコープはそれを取り囲むメソッドである.ブロック式を評価した結果はクロージャであり,他のすべてのものと同様に,クロージャもオブジェクトである.この場合,このオブジェクトは,ブロックの表すコードを実行するためのメソッドを持つ.

無名関数(クロージャ)はただかっこ良く見えたから,とか,他の言語でうまくいっているから,という理由で無目的にSmalltalkに導入された訳ではない.無名関数は,完全に,そして注意深く言語に統合されている.既存の言語に無名関数をうまく統合することも可能だが,初期の段階で統合することにはメリットがある.Guy Steelが一連の論文で示したように無名関数は,非常に強力で,他の言語機能を包含する.早い段階で無名関数を導入すれば,言語機能を追加する時間を節約して,ライブラリで実装することが可能になる.Smalltalkが言語として直接サポートしている制御構造はごくわずかである.条件文の if ですら,ライブラリメソッドとして実装されており,ブロックを用いて呼び出される.


Smalltalk のブロックとSchemeのそれとでは,2つの点が異なる.ひとつは,ブロック内のselfが,ブロックの外側のコンテクストでのselfを意味すること.特に,クロージャオブジェクトを指すわけではないこと.もう一つは,メソッドからリターンする「^ 式」 は,外側のメソッドからのリターンを意味しており,クロージャ実行のメソッドからのリターンを意味しないこと.これらの2点は,Schemeにはレキシカルスコープの言語要素が一つ(変数束縛)しかないのに対して,Smalltalkには3つのレキシカルスコープ言語要素を持っていることに起因している.3つとは,束縛(Schemeと同じ),リターン文の対象,"self"が指すもの,の3つである.上で示したクロージャの定義では,「自由変数の束縛」についてしか触れていないが,それはこの定義がSchemeのためになされたからであり,Schemeでは名前(変数)束縛だけが,レキシカルスコープの言語要素だからでる.Common Lispには"return"と"goto"があり,これらもクロージャ内でレキシカルに捕捉される.Guy Steeleのラムダ論文で書かれたとおりに,クロージャの力を存分に発揮させるためには,すべてのレキシカルスコープの言語要素をクロージャで捕捉しなければならない.他の言語をカバーできるようにクロージャの定義を一般化するには,より一般的な単語を使う必要があるだろう.「自由変数の束縛」のかわりに,例えば「レキシカルスコープの言語要素」を使えばいいかもしれない.しかしこれでは,クロージャという単語の出自が分からなくなってしまう.

さて,25年ほど早送りしてみよう.1970年代に聞いていたのと同じ音楽がまた流れるようになっていて,われわれは,JavaというSchemeよりもSmalltalkよりも格段に複雑な言語にクロージャを追加しようとしている.追加するのは,クロージャがかっこいいからでも,他の言語でうまくいっているからでも,ただ退屈したからでもない.クロージャが,プログラマの武器庫における強力で柔軟な武器になるからであり,他の既存の書き方で書くよりもプログラムがより読みやすくなることが期待できるからであり,最近提案された言語拡張の多くが,クロージャによって不要になるからだ.クロージャのパワーを完全に引き出すためには,レキシカルスコープ持つ言語要素をすべて捕捉しなければならない.Javaにおける,レキシカルスコープを持つ言語要素にはなにがあるだろうか?

  • 変数名の意味
  • メソッド名の意味
  • 型名の意味
  • thisの意味
  • 文ラベルの意味
  • ラベルのついていないbreak文が抜け出す場所
  • ラベルのついていないcontinue文が抜け出す場所
  • 宣言され,キャッチされる例外の集合
  • return文の戻り先
  • 変数へのアサインメント状態
  • 変数への非アサインメント状態
  • コードへの可達性

さらに,JavaにはSchemeSmalltalkと大きく異なった点がある.Javaは静的型付き言語なのである.すなわち,すべての式は,コンパイル時に型付けされていなければならない.したがって,クロージャを追加するのであれば,クロージャには適切な型がついていなければならない.クロージャは無名関数であるから,関数型を言語に追加するのが自然だ.しかしこれは必須というわけではない.われわれはクロージャ機構として2つバージョン(nominalとfunctional)を提案しているが,そのうちの一つが示す通り,関数型を追加しなくてもクロージャを追加することが可能だと考えている(高階プログラミングをするには実用にならないだろうが).

われわれの,クロージャ提案は,上記のチェックリストのすべてに対応している.提案には他の機能も含まれている(制御起動構文とクロージャ変換)が,これらは,直接クロージャの定義には関係ない.これらはクロージャを既存の言語機能とうまく統合させるためのものなのだ.さらに,規格のなかでは触れられていない追加機能(適切な末尾再起など)も,クロージャのパワーをフルに実現するために必要となるだろう.

さて,無名内部クラスはどうだろうか? 無名内部クラスは,上述のチェックリストのどれも満たさない.無名クラスでローカル変数を使用するには,外側のスコープでfinalを付けなければいけないということは,とりあえずおいておこう.問題は,変数名が正しいスコープで解決されていないということなのだ.変数名が,外側のスコープではなく定義された無名クラスの中で解決されてしまう.インターフェイスインスタンスを作るような場合には,これはあまり問題にはならない.多くのインターフェイスには変数(定数)は定義されていないからである.無名内部クラスは,チェックリストの他の項目すべてを満たさない.他のクロージャ提案のほとんども,このリストの項目に対応しようとしていないので,既存の言語要素と同様に,クロージャのパワーを活用することはできない.

これまでのべたプログラミング言語の理屈はさておき,無名内部クラスは実用上のクロージャとしての機能を果たすことができるだろうか? わたしはその答えはノーであると既に示したつもりだ.クロージャで書くことのできるすべてのプログラムにたいして,大体等価なプログラムを無名内部クラスで書くことはできるだろう.これは,Java言語がチューリング完全であるからである.しかしそのためには,プログラムの目的とは何の関係ものない,面倒で大々的なリファクタリングをしなければならないだろう.実際,アセンブラ言語ですら,大体等価なプログラムを書くことはできる.その努力に耐えられる気力があるならば,だが.これに対して,真のクロージャは,表現可能な抽象の種類を追加することで言語を強化してくれるのだ.