プログラミング言語 において、クロージャ(字句クロージャまたは関数クロージャとも呼ばれる)は、第一級関数を持つ言語で字句スコープの名前バインディングを実装するための手法です。操作的には、クロージャは関数[ a ]と環境[ 1 ]を格納するレコードです。環境は、関数の各自由変数(ローカルで使用されるが、囲んでいるスコープで定義されている変数)を、クロージャが作成されたときに名前がバインドされていた値または参照に関連付けるマッピングです。 [ b ]通常の関数とは異なり、クロージャを使用すると、関数がスコープ外で呼び出された場合でも、クロージャがコピーした値または参照を介して、キャプチャされた変数にアクセスできます。
クロージャの概念は、1960年代にλ計算における式の機械的評価のために開発され、1970年にPALプログラミング言語の言語機能として初めて完全に実装され、字句スコープの第一級関数をサポートしました。[ 2 ]
ピーター・ランディンは、1964 年に、式を評価するために彼のSECD マシンで使用されていた環境部分と制御部分を持つものとして、クロージャという用語を定義しました。 [ 3 ]ジョエル・モーゼスは、ランディンが、字句環境によって閉じられた (またはバインドされた) オープンバインディング (自由変数) を持つラムダ式を指す用語としてクロージャを導入したことを認めており、その結果、閉じられた式、つまりクロージャになります。[ 4 ] [ 5 ]この用法は、その後、 Lispの字句スコープの変種であるScheme を1975 年にサスマンとスティールが定義したときに採用され、広く普及しました。[ 6 ]
サスマンとアベルソンは1980年代にクロージャという用語を、別の、関連性のない意味でも使用しました。それは、データ構造にデータを追加する演算子が、入れ子になったデータ構造も追加できるという性質です。この用語の使用法は、コンピュータサイエンスにおける以前の使用法ではなく、数学での使用法に由来しています。著者らは、この用語の重複を「残念」だと考えています。[ 7 ]
クロージャという用語は、匿名関数の同義語としてよく使われますが、厳密には、匿名関数は名前のない関数リテラルであり、クロージャは関数のインスタンス、つまり値であり、その非ローカル変数は値または記憶 場所にバインドされています(言語によって異なります。下記の字句環境のセクションを参照してください)。
例えば、以下のPythonコードでは:
def f ( x : int ) -> Callable [[ int ], int ]: def g ( y : int ) -> int : return x + y return g # クロージャを返します。def h ( x : int ) -> Callable [[ int ], int ]: return lambda y : x + y # クロージャを返します。# 変数に特定のクロージャを割り当てます。a : Callable [[ int ], int ] = f ( 1 ) b : Callable [[ int ], int ] = h ( 1 )# 変数に格納されているクロージャを使用します。assert a ( 5 ) == 6 assert b ( 5 ) == 6# クロージャを最初に変数にバインドせずに使用します。assert f ( 1 )( 5 ) == 6 # f(1) はクロージャです。assert h ( 1 )( 5 ) == 6 # h(1) はクロージャです。aとの値はbクロージャであり、どちらの場合も、囲んでいる関数から自由変数を持つネストされた関数を返すことによって生成されます。そのため、自由変数は囲んでいる関数のパラメータの値にバインドされます。とxのクロージャは機能的に同一です。実装上の唯一の違いは、最初のケースでは という名前のネストされた関数を使用したのに対し、2番目のケースでは匿名のネストされた関数(匿名関数を作成するためのPythonキーワードを使用)を使用したことです。定義時に使用した元の名前(もしあれば)は関係ありません。abglambda
クロージャは他の値と同様に値です。変数に代入する必要はなく、例の最後の2行に示すように直接使用できます。この使用方法は「匿名クロージャ」とみなすことができます。
ネストされた関数定義自体はクロージャではありません。それらはまだ束縛されていない自由変数を持っています。外側の関数がパラメータの値で評価されたときに初めて、ネストされた関数の自由変数が束縛され、クロージャが作成されます。そして、そのクロージャが外側の関数から返されます。
最後に、クロージャは、非ローカル変数のスコープ外にある場合にのみ、自由変数を持つ関数と区別されます。それ以外の場合は、定義環境と実行環境が一致し、これらを区別するものは何もありません(名前が同じ値に解決されるため、静的バインディングと動的バインディングを区別することはできません)。たとえば、以下のプログラムでは、自由変数x(グローバルスコープの非ローカル変数にバインドされているx)を持つ関数は、が定義されているのと同じ環境で実行されるxため、これらが実際にクロージャであるかどうかは重要ではありません。
x : int = 1 nums : list [ int ] = [ 1 , 2 , 3 ]def f ( y : int ) -> int : return x + ymap ( f , nums ) map ( lambda y : x + y , nums )これは多くの場合、関数の戻り値によって実現されます。なぜなら、関数は非ローカル変数のスコープ内で定義される必要があり、その場合、通常は関数自身のスコープが小さくなるからです。
これは変数シャドウイング(非ローカル変数のスコープを縮小する)によっても実現できますが、実際にはあまり一般的ではなく、有用性が低く、シャドウイングは推奨されていません。この例では、はfクロージャであることがわかります。なぜなら、xの本体では、はのローカルではなく、グローバル名前空間のにfバインドされているからです。xxg
x : int = 0def f ( y : int ) -> int : return x + ydef g ( z : int ) -> int : x : int = 1 # ローカル変数 x はグローバル変数 x を隠蔽するreturn f ( z )print ( g ( 1 )) # 2ではなく1が出力されるクロージャの使用は、関数が第一級オブジェクトである言語と関連付けられています。このような言語では、関数は高階関数の結果として返されるか、他の関数呼び出しの引数として渡されます。自由変数を持つ関数が第一級である場合、自由変数を返すとクロージャが作成されます。これには、 LispやMLなどの関数型プログラミング言語、およびJulia、Python、Rustなどの多くの現代的なマルチパラダイム言語が含まれます。クロージャは、特にJavaScriptなどのイベントハンドラでコールバックとともによく使用され、動的なWebページとのやり取りに使用されます。
クロージャは、継続渡しスタイルで状態を隠すためにも使用できます。オブジェクトや制御構造などの構成要素は、クロージャを使用して実装できます。一部の言語では、関数が別の関数内で定義され、内側の関数が外側の関数のローカル変数を参照する場合にクロージャが発生することがあります。実行時に外側の関数が実行されると、内側の関数のコードと、クロージャが必要とする外側の関数の変数への参照(アップバリュー)からなるクロージャが形成されます。
クロージャは通常、第一級関数を持つ言語に現れます。つまり、そのような言語では、文字列や整数などのより単純な型と同様に、関数を引数として渡したり、関数呼び出しから返したり、変数名にバインドしたりすることができます。たとえば、次のScheme関数を考えてみましょう。
; 販売部数がTHRESHOLD以上の書籍のリストを返します。( define ( best-selling-books threshold ) ( filter ( lambda ( book ) ( >= ( book-sales book ) threshold )) book-list ))この例では、ラムダ式は(lambda (book) (>= (book-sales book) threshold))関数内に現れますbest-selling-books。ラムダ式が評価されると、Scheme はラムダ式のコードとthreshold変数への参照からなるクロージャを作成します。この変数はラムダ式内の自由変数です。
クロージャはfilter関数に渡され、関数はそれを繰り返し呼び出して、結果リストに追加する書籍と破棄する書籍を決定します。クロージャはへの参照を持っているためthreshold、呼び出しのたびにその変数を使用できますfilter。関数はfilter別のファイルで定義することもできます。
以下は、クロージャをサポートするもう一つの人気言語であるTypeScriptで同じ例を書き直したものです。
// 販売部数が「threshold」以上の書籍のリストを返します。function bestSellingBooks ( threshold : number ) : Book [] { return bookList . filter ( book => book . sales >= threshold ); }矢印演算子は、グローバル関数の代わりに矢印関数式とメソッド[ 8 ]=>を定義するために使用されますが、それ以外はコードの構造と効果は同じです。Array.prototype.filterfilter
関数はクロージャを作成して返すことができます。次の例をご覧ください。
type UnaryFunction = ( x : number ) => number ;//適切な大きさの区間 dx を使用してf の導関数を近似する関数を返します。 function derivative ( f : UnaryFunction , dx : number ) : UnaryFunction { return x => ( f ( x + dx ) - f ( x )) / dx ; }この場合、クロージャはそれを作成した関数の実行後も存続するため、実行がスコープを抜けて変数がもはや参照できなくなっても、関数が戻った後も変数fはdx存続します。クロージャのない言語では、自動ローカル変数の存続期間は、その変数が宣言されたスタックフレームの実行期間と一致します。クロージャのある言語では、既存のクロージャが変数への参照を持っている限り、変数は存在し続けなければなりません。これは、一般的には何らかのガベージコレクションを使用して実装されます。derivative
クロージャを使用すると、関数を「プライベート」変数のセットに関連付けることができます。これらの変数は、関数の複数回の呼び出しにわたって保持されます。変数のスコープはクロージャで囲まれた関数のみに限定されるため、他のプログラムコードからアクセスすることはできません。これらはオブジェクト指向プログラミングにおけるプライベート変数に相当し、実際には、クロージャは単一の呼び出し演算子メソッドを持つ状態を持つ関数オブジェクト(またはファンクタ)に似ています。
状態を持つ言語では、クロージャのアップバリュー(クロージャで閉じられた変数)は不定の範囲を持つため、ある呼び出しで確立された値が次の呼び出しでも利用可能となることから、クロージャを使用して状態表現と情報隠蔽のパラダイムを実装できます。このように使用されるクロージャは参照透過性を持たないため、純粋関数ではなくなります。しかしながら、 Schemeのような非純粋関数型言語では一般的に使用されています。
クロージャーには多くの用途があります。
( define foo #f ) ( define bar #f )( let (( secret-message "none" )) ( set! foo ( lambda ( msg ) ( set! secret-message msg ))) ( set! bar ( lambda () secret-message )))( display ( bar )) ; "none" と表示されます( newline ) ( foo "meet me by the docks at midnight" ) ( display ( bar )) ; "meet me by the docks at midnight" と表示されます注:一部の講演者は、字句環境を束縛するデータ構造をクロージャと呼ぶが、この用語は通常、関数を具体的に指す。
クロージャは通常、関数コードへのポインタと、クロージャが作成された時点での関数のレキシカル環境(つまり、使用可能な変数のセット)を表す特別なデータ構造を用いて実装されます。参照環境は、クロージャが作成された時点で、非ローカル名をレキシカル環境内の対応する変数にバインドし、さらにそれらの有効期間をクロージャの有効期間以上まで延長します。クロージャが後日、異なるレキシカル環境で実行される場合、関数は、現在の環境ではなく、クロージャによってキャプチャされた変数を参照する非ローカル変数を用いて実行されます。
言語の実装において、実行時メモリモデルがすべての自動変数を線形スタックに割り当てる場合、完全なクロージャを容易にサポートすることはできません。このような言語では、関数の自動ローカル変数は関数が戻るときに解放されます。しかし、クロージャでは、参照する自由変数が囲んでいる関数の実行後も存続する必要があります。そのため、これらの変数は、スタック上ではなく、通常はヒープ割り当てによって、不要になるまで存続するように割り当てられ、参照しているすべてのクロージャが使用されなくなるまで存続するように、そのライフタイムを管理する必要があります。
これは、クロージャをネイティブにサポートする言語が通常ガベージコレクションも使用する理由を説明しています。代替案としては、非ローカル変数の手動メモリ管理(ヒープに明示的に割り当て、完了したら解放する)か、スタック割り当てを使用する場合、C++11 のラムダ式[ 10 ]や GNU C のネストされた関数[ 11 ]のように、解放された自動変数へのダングリングポインタが原因で、特定のユースケースが未定義の動作につながることを言語が受け入れることです。 funarg問題(または「関数引数」問題)は、C や C++ のようなスタックベースのプログラミング言語で関数を第一級オブジェクトとして実装することの難しさを表しています。同様に、Dバージョン 1 では、プログラマはデリゲートと自動ローカル変数をどう扱うべきかを知っていると想定されています。定義スコープから戻った後、それらの参照は無効になるためです (自動ローカル変数はスタック上にあります)。これは、多くの有用な関数パターンを依然として許可しますが、複雑なケースでは変数の明示的なヒープ割り当てが必要です。 D言語バージョン2では、ヒープに格納する必要のある変数を検出し、自動的にメモリを割り当てることでこの問題を解決しました。D言語はどちらのバージョンでもガベージコレクションを使用するため、変数が渡される際の利用状況を追跡する必要はありません。
厳密な関数型言語(例えばErlang)では、変数の参照に循環参照が発生しないため、自動メモリ管理(ガベージコレクション)の実装が非常に容易です。例えばErlangでは、すべての引数と変数はヒープ上に割り当てられますが、それらへの参照はスタック上にも格納されます。関数が戻り値を返した後も、参照は有効なままです。ヒープのクリーンアップは、インクリメンタルガベージコレクタによって行われます。
MLでは、ローカル変数は字句スコープを持ち、スタックのようなモデルを定義しますが、オブジェクトではなく値にバインドされているため、実装者はプログラマーには見えない方法でこれらの値をクロージャのデータ構造にコピーすることができます。
Schemeは、ALGOLに似た字句スコープシステム、動的変数、ガベージコレクションを備えていますが、スタックプログラミングモデルを持たないため、スタックベースの言語の制約を受けません。Schemeではクロージャが自然に表現されます。ラムダ式はコードを囲み、その環境の自由変数はアクセス可能な限りプログラム内に保持されるため、他のScheme式と同様に自由に使用できます。
クロージャは、並行計算のアクターモデルにおけるアクターと密接に関連しており、関数のレキシカル環境内の値は「知り合い」と呼ばれます。並行プログラミング言語におけるクロージャの重要な問題は、クロージャ内の変数を更新できるかどうか、そして更新できる場合は、これらの更新をどのように同期できるかということです。アクターは、その解決策の1つを提供します。[ 12 ]
クロージャは関数オブジェクトと密接に関連しています。前者から後者への変換は、脱機能化またはラムダリフティングとして知られています。クロージャ変換も参照してください。
異なる言語では必ずしも共通の字句環境の定義がないため、クロージャの定義も異なる場合があります。一般的に受け入れられている最小限の字句環境の定義は、スコープ内のすべての変数の束縛の集合として定義されており、これはどの言語のクロージャでもキャプチャしなければならないものです。ただし、変数の束縛の意味も異なります。命令型言語では、変数は値を格納できるメモリ内の相対位置に束縛されます。束縛の相対位置は実行時に変わりませんが、束縛された位置の値は変わる可能性があります。このような言語では、クロージャが束縛をキャプチャするため、クロージャから実行されるかどうかにかかわらず、変数に対するすべての操作は同じ相対メモリ位置で実行されます。これはしばしば「参照による変数のキャプチャ」と呼ばれます。以下に、そのような言語の1つであるECMAScriptでこの概念を説明する例を示します。
let f : () => number ; let g : () => number ; function foo () { let x : number ; f = () => ++ x ; g = () => -- x ; x = 1 ; console . log ( `inside foo, call to f(): ${ f () } ` ); } foo (); // 2 console . log ( `call to g(): ${ g () } ` ); // 1 (--x) console . log ( `call to g(): ${ g () } ` ); // 0 (--x) console . log ( `call to f(): ${ f () } ` ); // 1 (++x) console . log ( `call to f(): ${ f () } ` ); // 2 (++x)関数fooと変数によって参照されるクロージャはすべてf、gローカル変数によって示される同じ相対メモリ位置を使用しますx。
場合によっては、上記の動作が望ましくない場合があり、別の字句クロージャをバインドする必要があります。ECMAScript では、これは . を使用して行いますFunction.bind()。
var module = { x : 42 , get () { return this . x ; } } var unboundGet = module . get ; console . log ( unboundGet ()); // 関数はグローバル スコープで呼び出されます// 'x' はグローバル スコープで指定されていないため、undefined が出力されます。var boundGet = unboundGet.bind ( module ); // オブジェクト module をクロージャとして指定console.log ( boundGet ( ) ); // 42 を出力この例では、各リンクがクリックされたときにそのIDが出力されることが期待される動作ですが、変数「e」は上記のスコープにバインドされ、クリック時に遅延評価されるため、実際には、クリックイベントごとに、forループの最後にバインドされた「elements」の最後の要素のIDが出力されます。[ 14 ]
var elements = document.getElementsByTagName ( ' a ' ); // 誤り: eは「handle」のクロージャではなく、 「 for 」ループを含む関数にバインドされていますfor ( var e of elements ) { e.onclick = function handle ( ) { console.log ( e.id ) ; } }ここでも、変数はまたはキーワードeを使用してブロックのスコープにバインドする必要があります。handle.bind(this)let
一方、MLなどの多くの関数型言語では、変数を値に直接バインドします。この場合、一度バインドされた変数の値を変更する方法がないため、クロージャ間で状態を共有する必要はなく、同じ値を使用します。これは、変数を「値渡し」でキャプチャすると呼ばれることがよくあります。Javaのローカルクラスと匿名クラスもこのカテゴリに属します。これらは、キャプチャされたローカル変数が である必要があるためfinal、状態を共有する必要もありません。
言語によっては、変数の値または場所のどちらかを選択してキャプチャできるものがあります。例えば、C++11では、キャプチャされた変数は[&]、参照キャプチャを意味する`<code>`で宣言するか[=]、値キャプチャを意味する`<code>`で宣言します。
さらに別のサブセットである、Haskellのような遅延評価関数型言語では、変数は値ではなく将来の計算結果にバインドされます。Haskellの例を見てみましょう。
-- Haskell foo :: Fractional a => a -> a -> ( a -> a ) foo x y = ( \ z -> z + r ) where r = x / yf ::分数a => a -> a f = foo 1 0main = print ( f 123 )r関数内で定義されたクロージャによって捕捉されるバインディングはfoo計算式に対応しており(x / y)、この場合はゼロ除算が発生します。しかし、捕捉されるのは値ではなく計算式であるため、エラーはクロージャが呼び出され、捕捉されたバインディングを使用しようとしたときにのみ発生します。
さらに、他の字句スコープ構造、例えばreturn、breakや文の動作にも違いが現れます。これらの構造は一般的に、囲んでいる制御文によって確立されたエスケープ継続continueを呼び出すものとして考えることができます(やの場合、このような解釈にはループ構造を再帰関数呼び出しとして考える必要があります)。ECMAScriptなどの一部の言語では、は文に対して字句的に最も内側にあるクロージャによって確立された継続を参照します。したがって、クロージャ内のは、それを呼び出したコードに制御を移します。しかし、Smalltalkでは、一見似ている演算子は、メソッド呼び出しのために確立されたエスケープ継続を呼び出し、間に挟まれたネストされたクロージャのエスケープ継続は無視します。特定のクロージャのエスケープ継続は、Smalltalkではクロージャのコードの終わりに到達することによってのみ暗黙的に呼び出すことができます。ECMAScriptとSmalltalkの次の例は、この違いを明確に示しています。breakcontinuereturnreturn^
"Smalltalk" foo | xs | xs := #( 1 2 3 4 ) . xs do: [ : x | ^ x ] . ^ 0 bar Transcript show: ( self foo printString ) "prints 1"// ECMAScript function foo () : number { let xs : number [] = [ 1 , 2 , 3 , 4 ]; xs . forEach ( function ( x ) { return x ; }); return 0 ; } console . log ( foo ()); // 0 と出力されます上記のコードスニペットは、Smalltalk の^演算子と JavaScript のreturn演算子が類似していないため、動作が異なります。ECMAScript の例では、ループreturn x;の新しい反復を開始するために内部クロージャを離れますforEachが、Smalltalk の例では、^xループを中止してメソッドから戻りますfoo。
Common Lisp は、上記のアクションのいずれかを表現できる構造を提供します。Lisp はSmalltalk(return-from foo x)のように振る舞い、Lisp はJavaScriptのように振る舞います。したがって、Smalltalk では、キャプチャされたエスケープ継続が、正常に呼び出される範囲を超えて存続することが可能です。以下を考えてみましょう。^x(return-from nil x)return x
「スモールトーク」foo ^ [ : x | ^ x ] bar | f | f := self foo . f value: 123 「エラー!」メソッドによって返されたクロージャfooが呼び出されると、クロージャを作成したメソッドの呼び出しから値を返そうとします。しかし、その呼び出しは既に値を返しており、Smalltalkのメソッド呼び出しモデルは複数の戻り値を容易にするためのスパゲッティスタックのfoo規則に従っていないため、この操作はエラーになります。
Rubyなどの一部の言語では、プログラマーがreturnキャプチャ方法を選択できます。Rubyの例を以下に示します。
# ルビー# Proc を使用したクロージャdef foo f = Proc . new { return "return from foo from inside proc" } f . call # 制御はここで foo から離れるreturn "return from foo" end# ラムダ式を使用したクロージャdef bar f = lambda { return "ラムダ式からの戻り値" } f . call # ここで制御は bar から離れませんreturn "bar からの戻り値" endputs foo # "return from foo from inside proc" と出力しますputs bar # "return from bar" と出力しますProc.newこの例では、と はどちらもlambdaクロージャを作成する方法ですが、作成されたクロージャの意味は、ステートメントに関して異なりますreturn。
Schemeでは、制御文の定義とスコープはreturn明示的です(ここでは便宜上「return」という名前を使用しています)。以下はRubyのサンプルをそのまま翻訳したものです。
; スキーム( define call/cc call-with-current-continuation )( define ( foo ) ( call/cc ( lambda ( return ) ( define ( f ) ( return "return from foo from inside proc" )) ( f ) ; ここで制御がfooを離れる( return "return from foo" ))))( define ( bar ) ( call/cc ( lambda ( return ) ( define ( f ) ( call/cc ( lambda ( return ) ( return "return from lambda" )))) ( f ) ; ここで制御はbarから離れません( return "return from bar" ))))( display ( foo )) ; "return from foo from inside proc" と出力します( newline ) ( display ( bar )) ; "return from bar" と出力します一部のプログラミング言語には、クロージャの動作をシミュレートする機能が備わっています。C ++、C#、D、Java、Objective-C、Visual Basic (.NET) (VB.NET) などの言語では、これらの機能は言語のオブジェクト指向パラダイムの結果として実現されています。
Cライブラリの中にはコールバックをサポートするものがあります。これは、ライブラリにコールバックを登録する際に、関数ポインタとvoid*ユーザーが選択した任意のデータへの別のポインタという 2 つの値を指定することで実装されることがあります。ライブラリがコールバック関数を実行すると、データポインタが渡されます。これにより、コールバックは状態を維持し、ライブラリに登録された時点で取得された情報を参照できるようになります。このイディオムは機能的にはクロージャに似ていますが、構文は異なります。ポインタは型安全void*ではないため、この C イディオムは C#、Haskell、ML の型安全なクロージャとは異なります。
コールバックは、グラフィカルユーザーインターフェース(GUI)ウィジェットツールキットで広く使用されており、グラフィカルウィジェット(メニュー、ボタン、チェックボックス、スライダー、スピナーなど)の一般的な機能を、アプリケーション固有の機能と関連付けることで、イベント駆動型プログラミングを実現します。
GNU Compiler Collection (GCC) 拡張機能を使用すると、ネストされた関数[ 15 ]を使用でき、関数ポインタでクロージャをエミュレートできます。ただし、関数が包含スコープから抜け出さないことが条件です。次の例は、adderトップレベルの定義であるため無効です (コンパイラのバージョンによっては、最適化なしでコンパイルした場合、つまり の場合、正しい結果が生成されることがあります-O0)。
#include <stdio.h>typedef int ( * FnIntToInt )( int ); // 関数 int->int の型FnIntToInt adder ( int number ) { int add ( int value ) { return value + number ; } return & add ; // C言語では関数名は自身を指すポインタなので、&演算子はここでは省略可能です}int main ( void ) { FnIntToInt add10 = adder ( 10 ); printf ( "%d \n " , add10 ( 1 )); return 0 ; }しかし、adder(そして必要に応じてtypedef) を移動することで、main有効になります。
#include <stdio.h>typedef int ( * FnIntToInt )( int ); // 関数 int->int の型int main ( void ) { FnIntToInt adder ( int number ) { int add ( int value ) { return value + number ; } return add ; } FnIntToInt add10 = adder ( 10 ); printf ( "%d \n " , add10 ( 1 )); return 0 ; }これを実行すると、11期待どおりの出力が得られます。
Javaでは、メソッド内でクラスを定義できます。これらはローカルクラスと呼ばれます。名前のないクラスは、匿名クラス(または匿名内部クラス)と呼ばれます。ローカルクラス(名前付きまたは匿名)は、字句的に囲むクラス内の名前、または字句的に囲むメソッド内の読み取り専用変数(としてマークされる)を参照できます。final
import java.net.URI ; import javax.swing.JFrame ;class CalculationWindow extends JFrame { private volatile int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // 式 "new Runnable() { ... }" は、'Runnable' インターフェイスを実装する匿名クラスです。Thread t = new Thread ( new Runnable () { @Override void run () { // final ローカル変数を読み取ることができます: calculate ( uri ); // 囲んでいるクラスのプライベートフィールドにアクセスできます: result += 10 ; } } ); t . start (); } }変数のキャプチャ機能をfinal使用すると、値による変数のキャプチャが可能になります。キャプチャ対象の変数が非キャプチャであっても、クラスの直前にfinal一時変数にコピーすることができます。final
参照による変数のキャプチャは、例えば要素が1つの配列など、可変コンテナへの参照を使用することでエミュレートできますfinal。ローカルクラスはコンテナ参照の値を変更することはできませんが、コンテナの内容を変更することは可能です。
Java 8のラムダ式の登場により、[ 16 ]クロージャによって上記のコードは次のように実行されるようになりました。
import java.net.URI ; import javax.swing.JFrame ;class CalculationWindow extends JFrame { private volatile int result ; // ... public void calculateInSeparateThread ( final URI uri ) { // コード () -> { /* コード */ } はクロージャです。Thread t = new Thread (() -> { calculate ( uri ); result += 10 ; }); t . start (); } }ローカルクラスは、メソッドの本体内で宣言される内部クラスのタイプの 1 つです。Java は、囲んでいるクラスの非静的メンバーとして宣言される内部クラスもサポートしています。 [ 17 ]これらは通常「内部クラス」と呼ばれます。[ 18 ]これらは囲んでいるクラスの本体内で定義され、囲んでいるクラスのインスタンス変数に完全にアクセスできます。これらのインスタンス変数へのバインドのため、内部クラスは、特別な構文を使用して囲んでいるクラスのインスタンスに明示的にバインドすることによってのみインスタンス化できます。[ 19 ]
public class EnclosingClass { // 内部クラスを定義します: public class InnerClass { public int incrementAndReturnCounter () { return counter ++ ; } }private int counter ; { counter = 0 ; }public int getCounter () { return counter ; }public static void main ( String [] args ) { EnclosingClass enclosingClassInstance = new EnclosingClass (); // インスタンスにバインドして内部クラスをインスタンス化しますEnclosingClass . InnerClass innerClassInstance = enclosingClassInstance . new InnerClass ();for ( int i = enclosingClassInstance . getCounter (); ( i = innerClassInstance . incrementAndReturnCounter ()) < 10 ; // インクリメントステップは省略) { System . out . println ( i ); } } }実行すると、0から9までの整数が出力されます。このタイプのクラスを、同じように宣言され、「static」修飾子が付随するネストされたクラスと混同しないように注意してください。ネストされたクラスは、期待される効果はなく、囲んでいるクラスで特別なバインディングが定義されていないだけのクラスです。
Java 8以降、Javaは関数を第一級オブジェクトとしてサポートしています。この形式のラムダ式は、java.util.function.Function<T, U>Tをドメイン、Uをイメージ型とする型とみなされます。この式はFunction.apply(T t)メソッドを使用して呼び出すことはできますが、標準的なメソッド呼び出しでは呼び出すことはできません。
import java.util.function.Function ;Function < String , Integer > length = s -> s . length (); System . out . println ( length . apply ( "Hello, world!" )); // '13' と出力されますAppleは、クロージャの一種であるブロックを、 C、C++、Objective-C 2.0、Mac OS X 10.6「Snow Leopard」、iOS 4.0に非標準の拡張機能として導入しました。Appleは、GCCおよびclangコンパイラ向けにその実装を提供しました。
ブロックとブロックリテラルへのポインタは でマークされます^。通常のローカル変数は、ブロックが作成されると値でキャプチャされ、ブロック内では読み取り専用になります。参照でキャプチャされる変数は でマークされます__block。作成されたスコープの外で永続化する必要があるブロックは、コピーする必要がある場合があります。[ 20 ] [ 21 ]
typedef int ( ^ IntBlock )();IntBlock downCounter ( int start ) { __block int i = start ; return [[ ^ int () { return i -- ; } copy ] autorelease ]; }IntBlock f = downCounter ( 5 ); NSLog ( @"%d" , f ()); NSLog ( @"%d" , f ()); NSLog ( @"%d" , f ());C# の匿名メソッドとラムダ式はクロージャをサポートしています。
System.Collections.Genericを使用します。int [] data = new []{ 1 , 2 , 3 , 4 }; int multiplier = 2 ; List < int > result = data . Select ( x => x * multiplier );C#と多くの類似した言語機能を持つVisual Basic .NETも、クロージャを用いたラムダ式をサポートしています。
Dim data = { 1 , 2 , 3 , 4 } Dim multiplier = 2 Dim result = data . Select ( Function ( x ) x * multiplier )D言語では、クロージャはデリゲートによって実装されます。デリゲートとは、関数ポインタとコンテキストポインタ(例えば、クラスインスタンス、またはクロージャの場合はヒープ上のスタックフレーム)がペアになったものです。
auto test1 () { int a = 7 ; return delegate () { return a + 3 ; }; // 匿名デリゲートの構築}auto test2 () { int a = 20 ; int foo () { return a + 5 ; } // 内部関数return & foo ; // デリゲートを構築する別の方法}void bar () { auto dg = test1 (); dg (); // 10 (OK: test1.a はクロージャ内にあり、まだ存在します)dg = test2 (); dg (); // 25 (OK: test2.a はクロージャ内にあり、まだ存在します) }D バージョン 1 では、クロージャのサポートが制限されています。たとえば、上記のコードは正しく動作しません。変数 a はスタック上にあり、test() から戻った後は、もはや有効ではないためです (おそらく、dg() を介して foo を呼び出すと、「ランダムな」整数が返されます)。これは、変数 'a' を明示的にヒープに割り当てるか、構造体またはクラスを使用して必要なすべてのクローズされた変数を格納し、同じコードを実装するメソッドからデリゲートを構築することで解決できます。クロージャは、参照される値が有効な間のみ使用される限り (たとえば、クロージャをコールバック パラメータとして別の関数を呼び出す場合)、他の関数に渡すことができ、汎用的なデータ処理コードを記述するのに役立つため、実際にはこの制限は問題にならないことがよくあります。
この制限は D バージョン 2 で修正されました。変数 'a' は内部関数で使用されているため、自動的にヒープに割り当てられます。また、その関数のデリゲートは (dg への代入または return によって) 現在のスコープから抜け出すことができます。デリゲートによって参照されない、または現在のスコープから抜け出さないデリゲートによってのみ参照されるその他のローカル変数 (または引数) はスタック上に残ります。これはヒープ割り当てよりも単純で高速です。関数の変数を参照する内部関数のクラス メソッドについても同様です。
C++では、オーバーロードによって関数オブジェクトを定義できますoperator()。これらのオブジェクトは、関数型プログラミング言語の関数にいくらか似た動作をします。実行時に作成でき、状態を保持できますが、クロージャのようにローカル変数を暗黙的にキャプチャしません。2011改訂版以降、C++ 言語はクロージャもサポートしています。クロージャは、ラムダ式と呼ばれる特別な言語構造から自動的に構築される関数オブジェクトの一種です。C++ クロージャは、アクセスされた変数のコピーをクロージャ オブジェクトのメンバーとして格納するか、参照によってコンテキストをキャプチャできます。後者の場合、クロージャ オブジェクトが参照されたオブジェクトのスコープから外れると、C++ クロージャはコンテキストの寿命を延長しないため、その呼び出しは未定義の動作を引き起こします。operator()
import std ;using std :: string ; using std :: vector ;void foo ( string & name ) { int n = 100 ; vector < string > v ; // ...// i は vector<string>::iterator 型ですauto i = std :: ranges :: find_if ( v , [ & ]( const string & s ) -> bool { return s != name && s . size () > n ; }); // 'i' は 'v.end()' か、'v' 内の最初の文字列を指しています// その文字列は 'name' と等しくなく、長さが 'n' より大きいものです}std::function<R(Args...)>C++ には匿名関数を保持するためのオブジェクトもあります。 [ 22 ]ただし、匿名関数はコンパイラによって生成された名前を持ち、区別されるため、それをとして格納するとstd::function型消去が使用され、わずかなオーバーヘッドが発生します。
Eiffelにはクロージャを定義するインラインエージェントが含まれています。インラインエージェントはルーチンを表すオブジェクトであり、ルーチンのコードをインラインで指定することによって定義されます。たとえば、
ok_button.click_event.subscribe ( agent ( x , y : INTEGER ) do map.country_at_coordinates ( x , y ) .display end )の引数はsubscribe、2 つの引数を持つ手順を表すエージェントです。この手順は、対応する座標にある国を見つけて表示します。エージェント全体は、click_event特定のボタンのイベント タイプに「登録」されているため、ユーザーがボタンをクリックするなどしてそのボタンでイベント タイプが発生するたびに、マウスの座標が と の引数として渡されて手順が実行されxますy。
Eiffelエージェントの主な制限は、他の言語のクロージャとは異なり、外側のスコープのローカル変数を参照できないことです。この設計上の決定により、クロージャ内のローカル変数の値について議論する際の曖昧さを回避できます。つまり、変数の最新の値なのか、エージェントが作成されたときに取得された値なのか、という問題を防ぐことができます。エージェント本体内からはCurrent、(Javaの に類似した)現在のオブジェクトへの参照this、その機能、およびエージェントの引数にのみアクセスできます。外側のローカル変数の値は、エージェントに追加のクローズドオペランドを提供することで渡すことができます。
Embarcadero C++Builder は、関数ポインタと同様の構文でメソッドへのポインタを提供する予約語を提供します。 [ 23 ]__closure
標準C言語では、関数型へのポインタのtypedefを以下の構文で記述できます。
typedef void ( * TMyFunctionPointer )( void ); 同様に、typedefメソッドへのポインタに対して、次の構文を使用して宣言できます。
typedef void ( __closure * TMyMethodPointer )();LISP における FUNCTION と QUOTE の違いを説明するのに役立つ比喩は、QUOTE を、自由変数が現在の環境に逃げ出すため、関数の多孔質または開いた被覆と考えることです。FUNCTION は閉じたまたは非多孔質の被覆として機能します (そのため、Landin が使用した用語は「クロージャ」です)。したがって、「開いた」ラムダ式 (LISP の関数は通常ラムダ式です) と「閉じた」ラムダ式について話[...] 環境問題への私の関心は、この問題を深く理解していたランディンが1966年から1967年にかけてMITを訪れた際に始まりました。その時、
LISP
の「閉じた」ラムダ式の評価結果であるFUNARGリストと
ISWIM
のラムダクロージャとの対応関係に気づいたのです。
「クロージャ」と呼ばれる理由は、自由変数を含む式は「開いた」式と呼ばれ、その式に自由変数の束縛を関連付けることで閉じられるからです。
包含関数が終了した後にアドレスを介してネストされた関数を呼び出そうとすると、大変なことになります。包含スコープ レベルが終了した後に呼び出しを試みて、スコープ外になった変数を参照している場合は、運が良ければ成功するかもしれませんが、リスクを冒すのは賢明ではありません。ただし、ネストされた関数がスコープ外になったものを参照していない場合は、安全です。