クロージャー
[Wikipedia|▼Menu]
□記事を途中から表示しています
[最初から表示]

セマンティクスの違い

言語ごとにスコープのセマンティクスが異なるように、クロージャの定義も異なっている。汎用的な定義では、クロージャが捕捉する「環境」とは、あるスコープのすべての変数の束縛の集合である。しかし、この変数の束縛というものの意味も言語ごとに異なっている。命令型言語では、変数は値を格納するためのメモリ中の位置と束縛される。この束縛は変化せず、束縛された位置にある値が変化する。クロージャは束縛を捕捉しているので、そのような言語での変数への操作は、それがクロージャからであってもなくとも、同一のメモリ領域に対して実行される。例として、ECMAScriptを取り上げるとvar f, g;function foo(){ var x = 0; f = function() { x += 1; return x; }; g = function() { x -= 1; return x; }; x = 1; console.log(f()); // "2"}foo();console.log(g()); // "1"console.log(f()); // "2"

関数 foo と2つのクロージャがローカル変数 x に束縛された同一のメモリ領域を使用していることに注意。

一方、多くの関数型言語、例えばML、は変数を直接、値に束縛する。この場合、一度束縛された変数の値を変える方法はないので、クロージャ間で状態を共有する必要はない。単に同じ値を使うだけである。

さらに、Haskellなど、遅延評価を行う関数型言語では、変数は将来の計算結果に束縛される。例を挙げる。foo x y = let r = x / y in (\z -> z + r)f = foo 1 0main = do putStr (show (f 123))

r は計算 (x / y) に束縛されており、この場合は0による除算である。しかしながら、クロージャが参照しているのはその値ではなく計算であるので、エラーはクロージャが実行され、実際にその束縛を使おうと試みたときに現れる。

さらなる違いは静的スコープである制御構文C言語風の言語における returnbreakcontinue などにおいて現れる。ECMAScriptなどの言語では、これらはクロージャ毎に束縛され、構文上の束縛を隠蔽する。つまり、クロージャ内からの return はクロージャを呼び出したコードに制御を渡す。しかしSmalltalkでは、このような動作はトップレベルでしか起こらず、クロージャに捕捉される。例を示して、この違いを明らかにする。"Smalltalk"foo 。xs 。 xs := #(1 2 3 4). xs do: [:x 。^x]. ^0bar Transcript show: (self foo) "prints 1"// ECMAScriptfunction foo() { var xs = new Array(1, 2, 3, 4); xs.forEach(function(x) { return x; }); return 0;}print(foo()); // prints 0

Smalltalkにおける ^ はECMAScriptにおける return にあたるものだと頭に入れれば、一目見た限りではどちらのコードも同じことをするように見える。違いは、ECMAScriptの例では return はクロージャを抜けるが関数 foo は抜けず、Smalltalkの例では ^ はクロージャだけではなくメソッド foo をも抜ける、という点である。後者の特徴はより高い表現力をもたらす。Smalltalkの do: は通常のメソッドであり、自然に制御構文が定義できている。一方、ECMAScriptでは return の意味が変わってしまうので、同じ目的には foreach という新しい構文を導入しなければならない。

しかし、スコープを越えて生存する継続には問題もある。foo ^[ x: 。^x ]bar 。f 。 f := self foo. f value: 123 "error!"

上の例でメソッド foo が返すブロックが実行されたとき、foo から値を返そうとする。しかし、foo の呼び出しは既に完了しているので、この操作はエラーとなる。
Ruby

Rubyなどの言語では、プログラマが return の振る舞いを選ぶことができる。def foo f = Proc.new { return "return from foo from inside proc" } f.call # control leaves foo here return "return from foo"end def bar f = lambda { return "return from lambda" } f.call # control does not leave bar here return "return from bar"end puts foo # prints "return from foo from inside proc"puts bar # prints "return from bar"

この例の Proc.new と lambda はどちらもクロージャを作るための方法である。しかし、それぞれが作ったクロージャの return の振る舞いに関しては、異なるセマンティクスを持っている。
Common Lisp

Common Lispでは、変数束縛を確立するlet、脱出点を確立するblock、Go Toのタグ(ラベル)を確立するtagbodyの三つの要素を基盤とし、これらの三つの組み合わせによって基本的な構文体系が構築されているが、それぞれの構文で確立された要素は、スコープとエクステント(存続期間)という概念によって整理されている。

これら、三つの構文の、変数名、ブロック名、ラベル名は、レキシカルスコープであり、クロージャに閉じ込めることができるが、変数束縛以外は、スコープ外(エクステント外)からアクセスすることはできない。Common Lispでは、これをレキシカルスコープかつ動的エクステントと表現する(変数はレキシカルスコープかつ無限エクステント)

blockにより確立された脱出点からは、return-fromによって抜け出す。また、tagbodyによって確立されたタグは、goにより参照される。(let ((m 3)) (defun a (x) ;; 関数定義は暗黙にblock名として関数名を設定する (* 3 (block b (* 100 (funcall (lambda (y) (block nil (tagbody (cond ((= 0 (mod y m)) (return-from a y)) ;mの倍数にはaから値をそのまま返す(m=3) ((oddp y) (return-from b (* 2 y))) ;奇数には二倍してbから脱出 (T (go exit))) ;どちらでもなければexitへgo toする ;;return-from nilの略記としてreturnが利用可能 exit (return y)))) ;lambda直下のblock nilから脱出 x))))))(a 1);--> 6(a 2);--> 600(let ((m 6)) ;;aの内部で参照するmは定義時のm (a 3));--> 3
C++

C++11規格以降でラムダ式が使えるようになった。なお、以下のようにローカル変数のキャプチャの方法を制御することができる。詳細はC++11を参照。#include <iostream>#include <vector>#include <string>#include <algorithm>void foo(std::string s) { int n = 0; // すべての自由変数をコピーキャプチャ。 auto func1 = [=]() { std::cout << n << ", " << s << std::endl; }; n = 1; s = ""; func1(); // すべての自由変数を参照キャプチャ。 auto func2 = [&]() { n = -1; s = "hoge"; }; func2(); std::cout << n << ", " << s << std::endl;}bool findName(const std::vector<std::string>& v, const std::string& name) { // 名前を指定して自由変数を参照キャプチャ。 auto it = std::find_if(v.begin(), v.end(), [&name](const std::string& s) { return s == name; }); return it != v.end();}
クロージャに類似した言語機能
C

C言語では、コールバックをサポートするライブラリ関数の中に、以下のように関数へのポインタと付随する任意のデータを指すためのポインタ(例えば汎用ポインタであるvoid*など)という2つの値を受け取るものがある。typedef int CallbackFunctionType(void* userData);extern int callUserFunction(CallbackFunctionType* callbackFunction, void* userData);

ライブラリ関数callUserFunctionがコールバック関数callbackFunctionを実行するたび、実行コンテキストとしてデータポインタuserDataを使用する。これによってコールバックは状態を管理することができ、登録した任意の情報を参照できる。このイディオムはクロージャと機能面で似ているが、構文面では似ていない。
C++

C++では、operator() をオーバーロードしたクラス(あるいは構造体)により、関数オブジェクトを定義できる。これは関数型言語における関数にいくらか似た振る舞いをみせる。C++の関数オブジェクトは非静的メンバー変数により状態を持つこともできる。しかし、一般的なクロージャのように自動的に(暗黙的に)ローカル変数を捕捉(キャプチャ)するようなことはしない。

また、ローカルクラス、すなわち関数内でクラスを定義することも可能だが、C++11よりも前の規格(C++03以前)ではテンプレート型引数として渡すことができなかったり、暗黙的に参照できる外のローカル変数は static 変数のみであり、自由変数のキャプチャを模倣するためには関数オブジェクトの非静的メンバー変数として明示的に保存しておく必要があったりするなど、後述する Java の無名クラス以上に制約条件が多い。C++11以降のラムダ式は、内部的にはコンパイラによる関数オブジェクトの自動生成により実現されている。したがって、自由変数をキャプチャする際には、関数オブジェクトであってもラムダ式であっても、変数寿命に配慮する必要がある。
Eiffel

Eiffelにはクロージャを定義するためのinline agent(インラインエージェント)がある。インラインエージェントはルーチンを表すオブジェクトで、次のように利用する。OK_button.click_event.subscribe( agent(x, y: INTEGER) do country := map.country_at_coordinates(x, y) country.display end)

subscribe の実引数はインラインエージェントで、2つの引数を持つ手続きである。ユーザがこのボタンをクリックして、click_event タイプのイベントが起こると、マウスの座標を引数としてこの手続きが実行される。

Eiffelのインラインエージェントの大きな限界は、外側のスコープのローカル変数を参照できないという点である。
Java 7 以前

Java 7 以前では、メソッド内部に「ローカルクラス」あるいは「匿名クラス」[6]を定義することで似たようなことができる。ローカルクラス/匿名クラスからは、そのメソッドの final (リードオンリー)なローカル変数を、ローカルクラス/匿名クラスのフィールドと名前が衝突しない限り、参照できる。class CalculationWindow extends JFrame { private JButton saveButton; ... public final void calculateInSeparateThread(final URI uri) { // "new Runnable() { ... }" で匿名クラスを記述する Runnable runner = new Runnable() { void run() { // 匿名クラスの外にあるfinalなローカル変数へアクセスする calculate(uri); // 内包するクラスのprivateフィールドにもアクセスできる // SwingのスレッドからGraphicsコンポーネントを更新する SwingUtilities.invokeLater(new Runnable() { public void run() { saveButton.setEnabled(true); } }); } }; new Thread(runner).start(); }}

要素が1つの配列を final な参照で保持すれば、クロージャで1つのローカル変数を参照する機能をエミュレートできる。内部クラスはその参照の値そのものを変えることはできないが、参照されている配列の要素の値は変えることができるからである。このテクニックはJavaに限ったものではなく、Pythonなど似た制限を持つ言語でも有効である。

Javaに完全なクロージャを追加するという言語拡張が検討されていた[7]。様々な問題により、クロージャを導入せずに、関数型インタフェース[8]を実装するための簡便な表記法(ラムダ式)が Java 8 にて導入された。
実装

クロージャは典型的には関数コードへのポインタ及び関数の作成時の環境の表現(例えば、使用可能な変数とその値の集合など)を含む特別なデータ構造によって実装される。

ある言語処理系の実行時のメモリモデルがすべてのローカル変数を線形なスタックに確保するものであれば、クロージャを完璧に実装するのは容易ではない。それは、以下のような理由による。
クロージャをつくった関数(エンクロージャ)の呼び出し元に復帰した際に、クロージャが参照するスタック上のローカル変数(レキシカル変数)が解放されてしまう。しかしクロージャにはレキシカル変数がエンクロージャの終了後も存続することが必要である。したがってレキシカル変数は必要がなくなるまで存続するように確保されなければならない。

クロージャが実行された時に、レキシカル変数のスタック上の位置を知ることは困難である。

第1の問題を解決するために、クロージャを実装するプログラミング言語は大抵、ガベージコレクションを備えている。この場合、クロージャへの参照が全て無効になった時に、レキシカル変数はガベージコレクタに渡される。

第2の問題を解決するためには、デリゲートのように、関数の参照と実行環境の参照をセットで扱える必要がある。


次ページ
記事の検索
おまかせリスト
▼オプションを表示
ブックマーク登録
mixiチェック!
Twitterに投稿
オプション/リンク一覧
話題のニュース
列車運行情報
暇つぶしWikipedia

Size:30 KB
出典: フリー百科事典『ウィキペディア(Wikipedia)
担当:undef